1. Omid Sayfun
  2. /
  3. Notebook
  • Home
  • About
  • Notebook
  • Token Usage
  • Whisper Usage
Tools
  • Agent Knowledge Base

Validating NestJS env vars with Zod

February 06, 2025 · Updated on August 09, 2026

Zod is the obvious choice for parsing request bodies, and environment variables deserve the same treatment. The difference is timing. You want env vars parsed once during boot, then used with confidence everywhere else. NestJS gives you a place to do that: the ConfigModule from @nestjs/config.

Register ConfigModule with a validate function

ConfigModule is a global module that reads your env file and loads it into the process. It also takes a validate function, which runs before the rest of the app boots. That hook is where Zod env validation goes:

import { ConfigModule } from '@nestjs/config';
 
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, validate: /* Your function */ }),
  ]
})
export class AppModule {}

Validate env vars with a Zod schema

The validate function receives the full process.env object and expects a validated, parsed object in return. So the schema is a plain z.object and the function is a one-liner. NestJS Zod validation for request bodies normally runs through a pipe or the nestjs-zod package; env vars skip both, because they only need to be parsed once:

import { z } from "zod";
 
const envs = z.object({
	DATABASE_URL: z.string(),
	NODE_ENV: z.string().optional(),
});
 
export const validate = (config: Record<string, unknown>) => {
	const validated = envs.parse(config);
	return validated;
};

If DATABASE_URL is missing, parse throws and the app never starts.

Extend ConfigService types with the same schema

You can also extend the ConfigService types with your Zod schema. That gives you type safety everywhere the ConfigModule is used, with no second source of truth. Here's how you define the type:

import type { ConfigService } from "@nestjs/config";
 
export type IConfigService = ConfigService<z.infer<typeof envs>>;

Call getOrThrow without casting

With the typed ConfigService in place, getOrThrow from @nestjs/config returns the schema type for a key instead of string | undefined, so no cast is needed at the call site. Use it in your modules like so:

@Module({
	imports: [
		DrizzlePostgresModule.registerAsync({
			tag: DB_TAG,
			imports: [ConfigModule],
			inject: [ConfigService],
			useFactory: (configService: IConfigService) => ({
				postgres: {
					url: configService.getOrThrow("DATABASE_URL"),
				},
			}),
		}),
	],
})
export class DbModule {}

tl;dr

  • Use NestJS's ConfigModule to load and validate env vars.
  • Register a validate function to ensure env vars are correct.
  • Use Zod to format and validate env vars.
  • Extend ConfigService types with Zod schema for type safety.
  • Implement the extended types in your modules for better type checking.

Join My Newsletter

Occasional notes on software, tools, and things I learn. No spam.

Unsubscribe anytime.

Continue Reading

  • Embeddings rot too. Running pgvector in production.Aug 28, 2026
  • Stop shipping retrieval changes on vibesAug 25, 2026
  • Your RAG is confidently wrong without hybrid searchAug 21, 2026
  • Stop tuning everything. pgvector has three knobs that matter.Aug 18, 2026
  • Your agent's knowledge base is lying to you. Run these 14 checks.Aug 18, 2026