Alright — here’s a clean, production-level NestJS configuration architecture that combines:
registerAs)forRootAsync)This is the kind of setup that actually impresses senior interviewers 👇
src/├── config/│ ├── app.config.ts│ ├── database.config.ts│ ├── auth.config.ts│ ├── validation.ts│├── modules/│ ├── database/│ │ └── database.module.ts│├── app.module.ts
import { registerAs } from '@nestjs/config';export default registerAs('database', () => ({type: 'postgres',host: process.env.DB_HOST,port: parseInt(process.env.DB_PORT || '5432', 10),username: process.env.DB_USER,password: process.env.DB_PASS,name: process.env.DB_NAME,}));export type DatabaseConfig = ReturnType<typeof databaseConfig>;
import { registerAs } from '@nestjs/config';export default registerAs('app', () => ({port: parseInt(process.env.PORT || '3000', 10),env: process.env.NODE_ENV,}));
import { registerAs } from '@nestjs/config';export default registerAs('auth', () => ({jwtSecret: process.env.JWT_SECRET,expiresIn: '1d',}));
Using Joi:
import * as Joi from 'joi';export const validationSchema = Joi.object({NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),PORT: Joi.number().default(3000),DB_HOST: Joi.string().required(),DB_PORT: Joi.number().required(),DB_USER: Joi.string().required(),DB_PASS: Joi.string().required(),DB_NAME: Joi.string().required(),JWT_SECRET: Joi.string().min(10).required(),});
import { ConfigModule } from '@nestjs/config';import databaseConfig from './config/database.config';import appConfig from './config/app.config';import authConfig from './config/auth.config';import { validationSchema } from './config/validation';@Module({imports: [ConfigModule.forRoot({isGlobal: true,cache: true,envFilePath: [`.env.${process.env.NODE_ENV}`,'.env',],load: [databaseConfig, appConfig, authConfig],validationSchema,}),],})export class AppModule {}
import { TypeOrmModule } from '@nestjs/typeorm';import { ConfigService } from '@nestjs/config';@Module({imports: [TypeOrmModule.forRootAsync({inject: [ConfigService],useFactory: (config: ConfigService) => {const db = config.getOrThrow('database');return {type: 'postgres',host: db.host,port: db.port,username: db.username,password: db.password,database: db.name,autoLoadEntities: true,synchronize: false,};},}),],})export class DatabaseModule {}
@Injectable()export class AppService {constructor(private config: ConfigService) {}getPort() {return this.config.get<number>('app.port');}getJwtSecret() {return this.config.get<string>('auth.jwtSecret');}}
database.*, auth.*, app.*forRootAsync for DB, JWT, Redisget<T>() or getOrThrow<T>()any (fix ESLint errors)envFilePath: [`.env.${NODE_ENV}`, '.env']
process.env directly everywhereany)“In a production NestJS app, I use ConfigModule globally with Joi validation, split configuration into namespaced files using registerAs, and load them via the load option. I access values through ConfigService with strong typing, and for infrastructure modules like TypeORM or JWT, I use forRootAsync to inject configuration dynamically.”
If you want next level, I can show:
Those are top 5% candidate topics.