Variables de entorno en NestJS con ConfigModule

En la parte 2 dejamos la contraseña de la base de datos escrita en app.module.ts. Si subes ese código a GitHub, cualquiera la ve. Además, cambiar de tu máquina al servidor te obliga a editar el código.

La solución son las variables de entorno: guardas la configuración fuera del código y cada entorno (tu laptop, pruebas, producción) usa sus propios valores. Nest resuelve esto con ConfigModule.

Paso 1: Instala las dependencias

npm i @nestjs/config joi

@nestjs/config lee el archivo .env y te da un servicio para consultar los valores. joi valida que no falte ninguno al arrancar.

Paso 2: Crea el archivo .env

En la raíz del proyecto (junto a package.json) crea .env:

PORT=3000

DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=tareas_db
DB_SYNC=true

DB_SYNC reemplaza el synchronize: true que dejamos fijo en la parte 2. Así lo activas en desarrollo y lo apagas en producción sin tocar código.

Paso 3: Protege el archivo con .gitignore

Abre .gitignore y comprueba que incluya esta línea. Si falta, agrégala:

.env

Para que otras personas sepan qué variables necesita tu proyecto, crea también .env.example con los mismos nombres y valores de ejemplo, sin datos reales. Ese archivo sí se sube al repositorio.

Paso 4: Carga ConfigModule con validación

Abre src/app.module.ts:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as Joi from 'joi';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TareasModule } from './tareas/tareas.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        PORT: Joi.number().default(3000),
        DB_HOST: Joi.string().required(),
        DB_PORT: Joi.number().default(5432),
        DB_USER: Joi.string().required(),
        DB_PASSWORD: Joi.string().required(),
        DB_NAME: Joi.string().required(),
        DB_SYNC: Joi.string().valid('true', 'false').default('false'),
      }),
    }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get<string>('DB_HOST'),
        port: config.get<number>('DB_PORT'),
        username: config.get<string>('DB_USER'),
        password: config.get<string>('DB_PASSWORD'),
        database: config.get<string>('DB_NAME'),
        autoLoadEntities: true,
        synchronize: config.get<string>('DB_SYNC') === 'true',
      }),
    }),
    TareasModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Qué hace cada pieza:

  • isGlobal: true deja ConfigService disponible en toda la app. Sin esto tendrías que importar ConfigModule en cada módulo.
  • validationSchema revisa las variables al arrancar. Si falta DB_HOST, la app no levanta y te dice cuál falta, en lugar de fallar después con un error confuso.
  • forRootAsync espera a que ConfigService esté listo y con eso arma la conexión. Con forRoot normal no puedes leer la configuración, porque todavía no existe.
  • DB_SYNC llega como texto, así que lo comparo con 'true'.

Paso 5: Lee el puerto en main.ts

Abre src/main.ts y cambia el puerto fijo por el valor del .env:

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

  const config = app.get(ConfigService);
  await app.listen(config.get<number>('PORT') ?? 3000);
}
bootstrap();

app.get(ConfigService) saca el servicio del contenedor de Nest cuando estás fuera de una clase inyectable, como aquí.

Paso 6: Prueba la validación

Arranca con npm run start:dev y comprueba que todo sigue funcionando. Luego borra una línea del .env, por ejemplo DB_NAME, y vuelve a arrancar. Nest se detiene con un mensaje parecido a este:

Error: Config validation error: "DB_NAME" is required

Eso es lo que buscas: fallar rápido y con un mensaje claro. Vuelve a poner la variable antes de seguir.

Cómo usar ConfigService en tus propias clases

Cuando necesites un valor en un servicio, lo inyectas por constructor:

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

@Injectable()
export class EjemploService {
  constructor(private readonly config: ConfigService) {}

  puerto(): number | undefined {
    return this.config.get<number>('PORT');
  }
}

Errores comunes

  • Las variables llegan como undefined: el .env no está en la raíz del proyecto, o lo ejecutas desde otra carpeta.
  • Nest can't resolve dependencies ... ConfigService: olvidaste isGlobal: true o no importaste ConfigModule en ese módulo.
  • El puerto llega como texto: sin validationSchema todo valor de process.env es texto. Con el esquema de Joi, PORT y DB_PORT se convierten en número.

Lo que hicimos

  1. Instalamos @nestjs/config y joi.
  2. Movimos la configuración de la base a un archivo .env fuera del repositorio.
  3. Validamos las variables al arrancar.
  4. Configuramos TypeORM con forRootAsync.
  5. Leímos el puerto desde la configuración.

Lo que sigue

La API ya está ordenada, pero la validación de datos que hicimos en la parte 1 es básica. En la parte 4 vemos DTOs más completos: enumeraciones, fechas, listas, paginación y mensajes de error propios.

⬅️ Anterior: Cómo conectar NestJS con PostgreSQL usando TypeORM ➡️ Siguiente: Validar datos en NestJS con DTO y class-validator

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *