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.
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:
- Porque es una mala práctica escribir datos de conexión, como el usuario o la clave de la base de datos, dentro del código de la aplicación. Inicialmente es por un problema de seguridad, ya que no quieres exponer las claves de sistemas críticos como el servidor de base de datos a posibles personas que lean el código, por ejemplo si lo publicas en un repositorio Git.
- Pero además, en la práctica las aplicaciones necesitan distintos valores para la conexión con la base de datos, por ejemplo. Una aplicación puede funcionar en local con unos datos de conexión con la base de datos, pero usar otros datos de login en remoto. Incluso es muy frecuente que varios desarrolladores trabajen sobre un mismo proyecto y muy probablemente las claves de acceso a sus sistemas gestores de base de datos serán completamente diferentes.
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:
- Acceso a las variables de entorno (
ConfigModule
) - Configuración de la conexión con la base de datos (
TypeOrmModule
)
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:
- imports: para importar otros módulos
- inject: para decir las cosas que querríamos inyectar
-
useFactory: es una función que devuelve el objeto de configuración para el acceso a la base de datos, donde podemos recibir el servicio
configService
inyectado.
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...