NestJS in Production · Configuration, Testing, Data and Operations
Validated Configuration and Dynamic Modules
Load configuration once, validate it at startup, and provide it through a dynamic module with forRoot().
What you'll build
A ConfigModule.forRoot() dynamic module that validates environment variables with Zod at startup and provides a typed AppConfig object, plus a MailModule.forRoot({ from }) that receives options from its importer. Invalid configuration stops the app with a readable message.
The idea
- Read and validate configuration once at startup; inject a typed object instead of reading
process.enveverywhere. - A dynamic module is a static method (
forRoot,register,forRootAsync) returning a module definition with providers built from options. global: truemakes a module's exports available everywhere (useful for configuration, use sparingly elsewhere).- Failing at startup is better than failing on the first request that needs a missing value.
The code
import "reflect-metadata";
import { DynamicModule, Inject, Injectable, Module } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { z } from "zod";
const Schema = z.object({
PORT: z.coerce.number().int().default(3000),
DATABASE_URL: z.url(),
MAIL_FROM: z.email(),
});
type AppConfig = z.infer<typeof Schema>;
const APP_CONFIG = Symbol("APP_CONFIG");
@Module({})
class ConfigModule {
static forRoot(env: Record<string, string | undefined>): DynamicModule {
const parsed = Schema.safeParse(env);
if (!parsed.success) {
throw new Error("invalid configuration: " + parsed.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; "));
}
return { module: ConfigModule, global: true, providers: [{ provide: APP_CONFIG, useValue: parsed.data }], exports: [APP_CONFIG] };
}
}
const MAIL_OPTIONS = Symbol("MAIL_OPTIONS");
@Injectable()
class MailService {
constructor(@Inject(MAIL_OPTIONS) private opts: { from: string }) {}
welcome(to: string) { return `From: ${this.opts.from} To: ${to} Subject: Welcome`; }
}
@Module({})
class MailModule {
static forRootAsync(): DynamicModule {
return { module: MailModule, providers: [MailService,
{ provide: MAIL_OPTIONS, inject: [APP_CONFIG], useFactory: (c: AppConfig) => ({ from: c.MAIL_FROM }) }], exports: [MailService] };
}
}
async function boot(env: Record<string, string | undefined>) {
@Module({ imports: [ConfigModule.forRoot(env), MailModule.forRootAsync()] })
class AppModule {}
const app = await NestFactory.createApplicationContext(AppModule, { logger: false });
console.log(app.get<AppConfig>(APP_CONFIG).PORT, "|", app.get(MailService).welcome("[email protected]"));
await app.close();
}
async function main() {
await boot({ DATABASE_URL: "postgres://db.internal/shop", MAIL_FROM: "[email protected]" });
try {
await boot({ PORT: "abc", MAIL_FROM: "nope" });
} catch (e) {
console.log((e as Error).message);
}
}
main();
Output
3000 | From: [email protected] To: [email protected] Subject: Welcome invalid configuration: PORT Invalid input: expected number, received NaN; DATABASE_URL Invalid input: expected string, received undefined; MAIL_FROM Invalid email address
Run this on your computer — see “Get Started” for setup.
Common mistakes
process.env.Xscattered through services, with no single place that lists required settings.- Defaults that hide missing production configuration (a default database URL pointing at localhost).
- Making many modules global, which hides dependencies; reserve it for configuration and similar infrastructure.
Key points
- Validate configuration at startup and inject a typed object.
- Dynamic modules (
forRoot,forRootAsync) build providers from options. useFactorywithinjectderives options from other providers such as config.
Further reading: Dynamic modules · Configuration
Practice questions
What is a dynamic module in NestJS?
Answer
A module created by a static method (forRoot, register, forRootAsync) that returns its definition with providers configured from the caller's options.
Why validate configuration at startup rather than on use?
Answer
Missing or invalid settings fail immediately with a clear message during deployment, instead of causing errors on a rarely used code path in production.
How do you build a provider from another provider's value?
Answer
A factory provider: { provide: TOKEN, inject: [OTHER_TOKEN], useFactory: (other) => ... }.
Open in the app to mark lessons complete, take notes and track your progress.