---
title: "Validating NestJS env vars with Zod"
description: "NestJS env validation with Zod instead of Joi: wire ConfigModule to a Zod schema, get full type safety from ConfigService, and use getOrThrow without casting."
canonical: https://omiid.me/notebook/38/validating-nestjs-env-vars-with-zod
author: Omid Sayfun
published: 2025-02-06
updated: 2026-08-09
---

# Validating NestJS env vars with Zod

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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
@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.
