nodejs · typescript · configuration · nestjs
Pasos sencillos para generar un módulo de configuración en NestJS
Cuando empecé con NestJS, no entendía los módulos, interceptors, guards y otras técnicas avanzadas que ofrece el framework. Como resultado, construí aplicaciones inseguras con malas prácticas. Con el tiempo, aprendí a las malas qué patrones realmente importan en aplicaciones de nivel de producción.
Después de varios años trabajando con NestJS, reuní algunas capacidades que quiero compartir. Una de ellas es crear un módulo de configuración personalizado donde se pueden declarar, cargar y validar variables de entorno. Con lo que se va a ver en este artículo, se pueden construir aplicaciones más robustas y listas para producción.
Configurando el proyecto
Primero, voy a instalar el Nest CLI de forma global para acceder a las capacidades del framework. Me gusta usar el CLI para aumentar mi productividad al trabajar con Nest.
npm install -g @nestjs/cli
Con el CLI instalado, ya se puede crear un nuevo proyecto de Nest usando el comando nest new. Prefiero trabajar con pnpm como package manager, pero se puede elegir el que mejor funcione en cada caso. Voy a llamar a mi proyecto config-module-example.
nest new config-module-example
Ahora voy a instalar el servicio de configuración de Nest.
pnpm i @nestjs/config
Con el proyecto listo, es momento de empezar a construir. Voy a empezar creando un nuevo módulo.
Creación del módulo
Como buena práctica, creo una carpeta común (common) para configuraciones, schemas de base de datos, definiciones, validaciones y otras utilidades que no son lógica de negocio. Empecemos por ahí.
Usando el CLI, voy a correr este comando:
nest g mo common/config
Este comando crea un módulo de configuración dentro de la carpeta common y un archivo config.module.ts donde voy a definir mi módulo de configuración personalizado—no hay que confundirlo con el que trae Nest por defecto.
Para hacer que mi módulo sea global, voy a agregar el decorador @Global para poder usarlo en toda la aplicación. También necesito importar el paquete @nestjs/config, pero renombrándolo como NestConfigModule para evitar confusión con mi propio módulo de configuración.
import { ConfigModule as NestConfigModule } from '@nestjs/config';
Ahora voy a configurar cómo se comporta el módulo. Como estoy trabajando con archivos .env, necesito indicarle al contenedor de Nest dónde encontrarlos y habilitar el caching. También necesito exportar este módulo para que se pueda usar como provider.
Con estas configuraciones en su lugar, mi módulo queda así:
@Global()
@Module({
imports: [
NestConfigModule.forRoot({
isGlobal: true,
envFilePath: [
`.env${process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''}`,
],
cache: true,
}),
],
exports: [NestConfigModule],
})
export class ConfigModule {}
Hay que prestar atención a la propiedad envFilePath—esta le indica al módulo que use el archivo .env específico del ambiente si NODE_ENV está presente, o el archivo por defecto si no lo está. Con mi módulo de configuración definido, ahora voy a definir los tipos con los que voy a trabajar.
Definiendo los tipos de configuración
Para este ejemplo, voy a declarar algunas variables de entorno básicas. Se pueden extender según haga falta.
Dentro de la carpeta common, voy a crear una carpeta de types con un archivo llamado app.types.ts. Ahí voy a definir el tipo AppConfig:
export type AppConfig = {
port: number;
nodeEnv: 'development' | 'production' | 'test' | 'staging';
};
Con los tipos definidos, ya se puede configurar la aplicación.
Trabajando con las configuraciones
Con los tipos definidos, ahora voy a crear el archivo de configuración. Dentro de la carpeta config, voy a crear una carpeta configurations y agregar un archivo llamado app.config.ts. Necesito importar la función registerAs desde @nestjs/config. Esta función registra un namespace de configuración que puedo referenciar más adelante. Así se ve:
export default registerAs(
'app',
(): AppConfig => ({
port: parseInt(process.env.PORT || '3000', 10),
nodeEnv: (process.env.NODE_ENV || 'development') as
| 'development'
| 'production'
| 'test'
| 'staging',
}),
);
Estoy usando registerAs con dos argumentos: el namespace token 'app' y una factory function que retorna el objeto AppConfig. Esto mapea mis variables de entorno a una configuración type-safe. Pero, ¿qué pasa si alguien carga un archivo .env corrupto con variables inválidas? Para evitar esto, el siguiente paso es definir y validar un schema.
Validando schemas con Joi
Con la configuración en su lugar, necesito asegurarme de que el archivo .env no tenga variables corruptas o inválidas. Para esto, voy a crear un schema con validaciones usando Joi.
Primero, voy a instalar Joi:
pnpm i joi
Después, voy a crear una carpeta de validaciones dentro de mi módulo de configuración para guardar mis schemas. Usando Joi, voy a definir el schema de validación:
export const appValidationSchema = Joi.object({
PORT: Joi.number().default(3000),
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
});
El schema valida que PORT sea un número y que NODE_ENV sea uno de los valores de ambiente permitidos, con defaults sensatos para ambos. Con todas las piezas listas, es momento de conectar todo y hacer que esta configuración funcione.
Conectando todo
Ahora voy a conectar todas las piezas. Voy a volver al archivo config.module.ts y agregar el cargador de configuración y el schema de validación:
@Global()
@Module({
imports: [
NestConfigModule.forRoot({
isGlobal: true,
load: [appConfig],
validationSchema: appValidationSchema,
envFilePath: [
`.env${process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''}`,
],
cache: true,
}),
],
exports: [NestConfigModule],
})
export class ConfigModule {}
La propiedad load le indica al módulo qué archivos de configuración cargar, y validationSchema asegura que las variables de entorno sean válidas antes de que arranque la app. Con el módulo completo, probemos que funciona.
Probando que la configuración funciona
Para probar la configuración, voy a loguear el puerto y el ambiente al arrancar. En mi archivo main.ts, voy a inyectar el ConfigService y obtener los valores de configuración:
const configService = app.get(ConfigService);
const port = configService.get<number>('app.port', { infer: true }) as number;
const nodeEnv = configService.get<string>('app.nodeEnv', { infer: true }) as string;
if (nodeEnv === 'development') {
console.log(`Application is running in ${nodeEnv} mode`);
console.log(`Listening on port ${port}`);
}
Antes de correr la aplicación, voy a crear un archivo .env con las variables de entorno:
PORT=3000
NODE_ENV=development
Ahora voy a correr pnpm run start:dev. Si todo está configurado correctamente, la aplicación va a arrancar y mostrar los logs de desarrollo.
Reflexiones finales
Crear un módulo de configuración personalizado en NestJS es esencial para construir aplicaciones robustas y mantenibles. En este artículo se mostró cómo estructurar variables de entorno, validarlas con schemas de Joi y organizarlas de una forma que escala junto con el proyecto.
Esta configuración evita los errores que cometí al principio de mi camino con NestJS. Las configuraciones type-safe con validación detectan errores antes de que lleguen a producción, haciendo que las aplicaciones sean más confiables y fáciles de mantener.
El código completo está disponible en mi repositorio de Github: https://github.com/RubenOAlvarado/config-module-example
¡A programar—nos vemos en el próximo!