> Manuales > Manual de NestJS

Qué son las variables de entorno y por qué son tan importantes en las aplicaciones. Cómo gestionar las variables de entorno en NestJS y cómo aplicar los servicios de configuración para los datos de acceso a bases de datos.

Gestión de variables de entorno para configuración de aplicaciones NestJS los datos de conexión

Las variables de entorno son una de las piezas esenciales de las aplicaciones backend en general. Prácticamente todas las aplicaciones las van a utilizar. Por supuesto, Nest te ofrece sus mecanismos específicos para que puedas trabajar con ellas.

Lidiar con variables de entorno para distintos tipos de configuraciones es esencial por un par de motivos:

Gestionar variables de entorno en Node es muy sencillo y no necesitamos realmente nada en especial, es decir, no es necesario un package para poder conseguirlo. Sin embargo, NestJS ya tiene su propia administración de variables de entorno, que permite muchas facilidades más, que si lo hacemos de manera nativa. En este artículo te explicaremos cómo sacarle partido al módulo de configuración de Nest y cómo aplicarlo para guardar de manera externa las variables de conexión con la base de datos****.

Instalar el módulo de configuración de Nest

Para usar esta utilidad tenemos que instalar un paquete extra llamado "config".

npm i @nestjs/config

Por si a alguien le interesa, Nest usa por debajo un paquete llamado "dotenv", que es bastante popular en el desarrollo de aplicaciones bajo esta plataforma.

Crear un archivo .env para las variables de entorno

Ahora podemos crear un archivo de variables de entorno en la raíz del proyecto, que debe de tener el nombre ".env". Este archivo tendrá todas las variables que sean necesarias para la aplicación, aunque de momento solamente vamos a colocar los datos de conexión a MySQL.

DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USER=mi_usuario
DB_PASSWORD=secret
DB_NAME=nestdb

Aunque esperamos que la mayoría ya lo sepa, dejaré un par de notas sobre archivos .env para quien no tenga experiencia con ellos. Primero decir que el archivo se llama .env, tal cual. No es que sea una extensión de un archivo como algo.env. Ten en cuenta además que los archivos que comienzan por "." en sistemas como Linux o Mac son archivos ocultos, por lo que no los podrás ver con el explorador de archivos típico como Finder en MacOS. Si abres la carpeta con un editor, como VSCode, sí que verás los archivos que aparecen comenzando por ".".

Leer la configuración desde el módulo principal de la aplicación

A continuación vamos a trabajar sobre nuestro módulo principal, el archivo app.module.ts. Para poder leer estas variables de entorno desde el módulo necesitamos usar ConfigModule, que básicamente es otro módulo de Nest que ofrece justamente el acceso a las variables y valores almacenados en el .env.

Para comenzar, necesitaremos importar el módulo ConfigModule, que viene de @nest/config.

import { ConfigModule } from '@nestjs/config';

Ahora, en la lista de módulos del AppModule necesitamos indicar que vamos a usar ConfigModule y este módulo se encargará de traernos los datos del archivo de entorno .env que acabamos de crear en el sistema.

En el decorador del módulo principal, AppModule, tendremos que declarar el ConfigModule dentro de la declaración de imports. Además vamos a invocar el método forRoot() de este módulo, que quedará de esta manera.

@Module({
  imports: [ConfigModule.forRoot()],
  // ...
})

Este módulo además se encargará de mezclar las configuraciones del archivo .env con las de process.env (nativas de Node) y guardar estos datos dentro de un ConfigService. El método forRoot() es el que se encarga de registrar el ConfigService, que contiene un método llamado get() que puedes usar para obtener los valores de las variables de configuración.

Opciones de configuración de la carga del .env

Existen varias alternativas de configuración del módulo ConfigModule, como cargar varios archivos a la vez, cambiar la ruta donde está el .env y otras cosas. Lo mejor es que revises la propia documentación del módulo config de Nest.

Hay una configuración que sí queremos remarcar aquí, que consiste en hacer global el ConfigModule.

ConfigModule.forRoot({
  isGlobal: true,
});

Esto sirve para que no tengas que importar todo el rato el ConfigModule en otros módulos de Nest donde lo necesites usar.

Inyectar el servicio para el acceso a las variables de entorno

Gracias a que hemos importado el ConfigModule en un módulo en concreto, estamos en disposición de usar el servicio ConfigService allá donde lo necesitemos.

La inyección del servicio se hace en el constructor, tal como hemos aprendido con otros servicios anteriormente.

constructor(private configService: ConfigService) { }

Recuerda que para disponer de este servicio en el controlador debes haber hecho el import del módulo ConfigModule en la declaración de "imports" del @Module(), a no ser que hayas hecho global el ConfigModule gracias a la configuración isGlobal que acabamos de mencionar.

Obtener variables de configuración

Ahora, en los métodos de la clase donde has inyectado configService, puedes acceder a los datos de las variables de entorno mediante el método get(), de esta manera.

let port = this.configService.get('DB_PORT')

Ten en cuenta que, aunque en el .env el dato DB_PORT es numérico, lo que tienes en como resultado de hacer this.configService.get('DB_PORT') es una cadena de caracteres.

Podemos enviar además un segundo parámetro al método get() para indicarle un valor predeterminado, que se tomará en caso que no se encuentre la variable de configuración que se ha seleccionado.

this.configService.get('OTRA_COSA', 'xyz');

Usar las variables de entorno desde un módulo

Hemos visto cómo inyectar el configService en una clase. Esto nos permitiría acceder a la configuración desde un controlador o un servicio, por ejemplo.

Pero ¿Qué pasa cuando queremos acceder a los datos de configService desde un módulo? Podríamos inyectar también el servicio, pero no es factible porque generalmente en el módulo no tienes código, sino que se define todo en el propio decorador @Module().

Es decir, el mismo decorador @Module() donde estamos importando ConfigModule puede necesitar acceder directamente al servicio. Por tanto, no hay un constructor por medio donde tengamos la posibilidad de inyectar nada.

El ejemplo que vamos a ver para ilustrar esta situación es cuando realizamos la conexión con la base de datos mediante TypeORM. Esos datos de conexión los queremos recibir desde las variables de entorno y por tanto, necesitamos acceder al configService para conseguirlo.

En el módulo principal de la aplicación, AppModule, es donde estamos declarando los módulos de:

El código que tendríamos es algo como lo que sigue:

@Module({
  imports: [
    ConfigModule.forRoot(),
    ProductsModule, 
    TagsModule, 
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: '192.168.10.10',
      // … otros valores de conexión 
    }
    )],
  controllers: [AppController],
  providers: [AppService],
})

Así pues, en el mismo @Module necesito hacer el acceso al service, para poder alimentar TypeOrmModule.forRoot() con variables que nos vienen el .env. ¿Cómo lo consigo?

Esto se soluciona mediante otro método de TypeOrmModule() llamado forRootAsync(), que sustituye a forRoot() y sirve para hacer una conexión con la base de datos, pero asíncrona.

forRootAsync() es necesario porque, al trabajar con este módulo de configuración necesitamos asegurarnos que el archivo .env se lea antes de usar las variables de entorno, y tengamos disponible el servicio configService con los datos cargados. Esa carga de las variables de entorno es asíncrona..

Dentro de forRootAsync() tenemos la posibilidad de entregarle un objeto con diversas propiedades, que son las que nos permiten importar módulos como ConfigModule o inyectar dependencias como el servicio configService. Las propiedades del objeto que enviaremos a forRootAsync() son estas:

En la siguiente porción de código vemos cómo se usa forRootAsync():

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: async (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('DB_HOST'),
    port: configService.get('DB_PORT'),
    username: configService.get('DB_USER'),
    password: configService.get('DB_PASSWORD'),
    database: configService.get('DB_NAME'),
    retryDelay: 3000,
    autoLoadEntities: true,
    synchronize: true,
  })
})

El código puede quedar un poco lioso, pero se trata de una fórmula sencilla en realidad, que te servirá en todas las aplicaciones donde quieras usar las variables de entorno para la configuración del acceso mediante TypeORM.

Otros módulos que pueden requerir el configService

Ten en cuenta que forRootAsync() es un método particular de TypeOrmModule. Si intentas usar configService en otros módulos puede que el método que tengas que implementar se llame de otra manera. Por ejemplo, HttpModule tiene un método registerAsync() que sirve para hacer lo mismo.

HttpModule.registerAsync({
  // aquí usamos imports, inject y useFactory
})

Con esto queremos decir que es importante leer la documentación del módulo que quieres usar para saber cómo tienes que hacer para acceder a los datos de configuración desde un módulo.

Miguel Angel Alvarez

Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...

Manual