NestJS Tips

CLI

La CLI o Línea de comandos es una de las cosas que nos hará más simple la vida pues nos permite de forma rápida crear archivos con la estructura necesaria par el correcto funcionamiento con el framework. Es algo que actualmente los frameworks o librerías enfocadas a enterprise ven ya como parte de un mínimo para su distribución. Lo principal es el comando

nest generate / nest g

Con el cual podremos crear diferentes tipos de elementos en nuestro proyecto.

Archivos generables

Nombre Alias Descripción
controller co Crear un controlador simple en la ruta que le demos
provider pr Crear un proveedor o servicio
pipe pi Genera el código para un nuevo pipe
gateway ga Podemos crear un gateway para el uso de Socket.io
guard gu Como en Angular los Guards son para la seguridad y esto nos dará lo básico en el nuevo

Existen muchos más comandos. Si deseas ver la lista completa ingresa a: https://docs.nestjs.com/cli/usages

Configuración

Algo importante al desarrollar software con altos estándares es no "quemar" datos en código tales como la dirección de la base de datos, el usuario, etc. Para esto existen varias prácticas y en este caso lo hacemos a través de un archivo .env.

Para esto lo primero que haremos es instalar un par de dependencias en nuestro proyecto

npm i --save @nestjs/config

Una vez instalada esta dependencia empezaremos creando un archivo .env en la raíz de nuestro proyecto que se verá algo así

PORT=3000
CORS='true'
LOGGER='true'
DB_HOST='xxx.xxx.xxx.xxxx'
DB_PORT='xxxx'
DB_USER='xxxxxxxx'
DB_PASSWORD='xxxxxxxxxxx'
DB_DATABASE='xxxxxxxxx'

Ahora que tenemos este archivo vamos a agregar uno en ./src con el nombre config.ts. La idea de este segundo archivo es convertir esta data del archivo .env en un JSON que sea fácilmente legible en el futuro desde cualquier parte de la aplicación.

export const config = () => ({
  port: Number(process.env.PORT),
  cors: process.env.CORS === 'true',
  logger: process.env.LOGGER === 'true',
  database: {
    type: 'mysql',
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_DATABASE,
    },
});

Ahora que hemos preparado los recursos necesarios podemos integrar la configuración a nuestro proyecto.

Iremos a nuestro ./src/app.module.ts y en la sección de imports agregaremos el módulo de configuración de la siguiente manera.

...
import { ConfigModule } from '@nestjs/config';
import { config } from './config'
....
@Module({
    imports: [
        ...
        ConfigModule.forRoot({
            isGlobal: true,
            load: [config]
        }),
        ...
    ],Ï
    ...
})
export class AppModule {}

Una vez que agregamos esta instrucción la configuración que es cargada por el ConfigModule es convertida en JSON y puesta a disponibilidad de toda la aplicación.

Sin embargo necesitamos algunos de estos valores antes de iniciar la aplicación en nuestro archivo ./main.ts, por lo que haremos lo siguiente:

import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  const cors = configService.get('CORS') === 'true';
  if (cors) {
    app.enableCors();
  }
  const logger = configService.get('LOGGER') === 'true';
  if (logger) {
    app.useLogger(new Logger());
  }
  await app.listen(configService.get('PORT'));
  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

En el caso de que utilices TypeORM como yo, te dejo este pequeño truco: Para activar su configuración con este mismo archivo .env. Lo primero que debes hacer es agregar un archivo en el mismo ./src el cual llamaremos database.config.ts y agregaremos lo siguiente:

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmOptionsFactory } from '@nestjs/typeorm';

@Injectable()
export class DatabaseConfig implements TypeOrmOptionsFactory {
  constructor(private configService: ConfigService) {}

  createTypeOrmOptions() {
    const config = this.configService.get('database');
    return config;
  }
}

De esta manera el archivo tomará el servicio que nos da el ConfigModule y podremos leer la configuración desde el JSON que designamos. Incluso para hacerlo más eficiente vemos que se obtiene solo la sección de database, ya que en este caso es lo único que nos interesa.

Ahora en nuestro archivo de ./src/app.module.ts vamos a modificar nuestra carga del módulo:

...
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { config } from './config';
import { DatabaseConfig } from './database.config';
....
@Module({
    imports: [
        ...
        ConfigModule.forRoot({
            isGlobal: true,
            load: [config]
        }),
        TypeOrmModule.forRootAsync({
            imports: [ConfigModule],
            useClass: DatabaseConfig
        }),
        ...
    ],Ï
    ...
})
export class AppModule {}  ****

Es importante notar que primero debe cargar el ConfigModule antes del TypeOrmModule.

Logger

En las aplicaciones de servidor es muy importante tener un tracking continuo de las acciones de la misma para que, de ser necesario, encontremos el error o la irregularidad cuanto antes.

Para esto podremos aprovechar el módulo de configuración que vimos anteriormente y agregaremos cosas como los niveles de registros que queremos utilizar sin tener que modificar el código de la aplicación.

Lo primero para esto es agregar una línea a nuestro archivo .env:

...
LOGGER_LEVELS='error,warn,log,verbose,debug'
...

Estos son los 5 niveles que nos dará el framework y los he ordenado según su prioridad, al menos en mi opinión. A continuación agregaremos estos datos a nuestro JSON en ./src/config.ts:

export const config = () => ({
    ...
    loggerLevels: process.env.LOGGER_LEVELS.split(',') || [], 
    ...
});

Ya hemos preparado el terreno, pero continuaremos creando nuestro propio provider el cual podrá ser importado por los otros providers y controllers fácilmente. Para esto usaremos nuestro CLI con el comando:

nest g pr providers/mylogger/mylogger.provider

El cual nos dará un archivo que para cuando terminemos de modificarlo debería verse algo así:

import { Injectable, Logger, Scope } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable({ scope: Scope.TRANSIENT })
export class MyLoggerProvider extends Logger {
  levels: string[] = ['error', 'warn', 'log', 'verbose', 'debug'];
  constructor(private config: ConfigService) {
    super();
    this.levels = this.config.get('loggerLevels');
  }

  log(message: string, context?: string) {
    if (this.levels.includes('log')) {
      super.log(message, context);
    }
  }
  error(message: string, trace?: string, context?: string) {
    if (this.levels.includes('error')) {
      super.error(message, trace, context);
    }
  }
  warn(message: string, context?: string) {
    if (this.levels.includes('warn')) {
      super.warn(message, context);
    }
  }
  debug(message: string, context?: string) {
    if (this.levels.includes('debug')) {
      super.debug(message, context);
    }
  }
  verbose(message: string, context?: string) {
    if (this.levels.includes('verbose')) {
      super.verbose(message, context);
    }
  }
}

Como podemos ver lo primero es que hemos cambiado el tipo de Scope para la Inyección del Provider por una transitoria, lo cual permite que cada cliente de este Provider obtenga una instancia independiente. Esto lo queremos así pues nos permitirá manejar el contexto del cliente y saber quien escribió cada registro.

Después observamos que este provider deberá extender de la clase nativa de NestJS Logger la cual nos dará acceso a los métodos de cada tipo de registro.

Finalmente en el constructor de la clase vemos como obtenemos el servicio o provider del ConfigModule con el cual podemos leer la información que nos interesa. Esta información es validada en cada tipo de registro para saber si debe o no ejecutarse la función.

Como último paso iremos a nuestro archivo ./src/main.ts y haremos un pequeño cambio para configurar nuestro nuevo Logger:

import { MyLoggerProvider } from 'providers/mylogger/mylogger.provider';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
    ...
  if (logger) {
    app.useLogger(new MyLoggerProvider(configService));
  }
    ...
}
bootstrap();

Esta es una herramienta que ha ganado mucho uso en la documentación de las aplicaciones de Backend debido a lo simple que es poder visualizar y probar los diferentes endpoints de una API.

En NestJS esto se puede agregar fácilmente con unos pequeños ajustes.

Primero vamos a instalar algunas dependencias.

npm i --save @nestjs/swagger swagger-ui-express

Una vez instalado esto procedemos a nuestro archivo ./main.ts para habilitarlo:

import { MyLoggerProvider } from 'providers/mylogger/mylogger.provider';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
    ...
  const config = new DocumentBuilder()
      .setTitle('My new API')
      .setDescription('Swagger for my new API')
      .setVersion('1.0')
      .addBearerAuth(
            { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
              'XYZ',
      )
      .build();
      const document = SwaggerModule.createDocument(app, config);
      SwaggerModule.setup('swagger', app, document);
    ...
}
bootstrap();

Esto levantará un servicio de Swagger en /swagger para que puedas probar tus endpoints de una forma amigable. Es importante destacar que por ejemplo, aquí estamos agregando la autenticación de JWT al Swagger para que puedas usar las APIs privadas.

En los controladores que se autentican con JWT, tendremos que agregar un decorador que puede ir en el controlador o bien en cada una de las funciones por lo que se verá algo así:

// Controllers
@ApiBearerAuth('XYZ')
@UseGuards(JwtAuthGuard)
@Controller('settings')

// Funciones
@ApiBearerAuth('XYZ')
@UseGuards(JwtAuthGuard)
@Get()

Documentación

Sé que hacer la documentación de un proyecto es algo que a la mayoría de desarrolladores no nos encanta. Justamente por eso creo que está herramienta es de mis favoritas. En un simple comando tendremos una documentación bastante rigurosa, entendible e incluso interactiva del proyecto gracias al generador compodoc. Para ello solo escribiremos lo siguiente en la raíz de nuestro proyecto.

npx @compodoc/compodoc -p tsconfig.json -s

Esto genera una carpeta documentation en la raíz del proyecto y levanta un servicio HTTP para que podamos verlo en http://localhost:8080 :

Yo sugiero guardar este comando como parte de nuestros scripts en el archivo package.json

Bueno creo que con estos son bastantes tips y herramientas que podemos empezar a integrar en nuestros proyectos con este framework. Si bien tengo algunos más creo que los dejaré para un próximo post.

Priméro quisiera saber que les han parecido los que hemos visto y que me digan si quieren que investigue específicamente alguna funcionalidad.

24