@alireza_ghasemi/env-typecheck
v1.0.0
Published
Type-safe environment variables with Zod — for Express, NestJS and any TypeScript project
Maintainers
Readme
env-typecheck
Type-safe environment variable validation for TypeScript projects, powered by Zod and dotenv.
Use it in Express, NestJS, CLIs, workers, queues, scripts, or any Node.js app that needs validated configuration at startup.
Why env-typecheck?
Environment variables are always strings at runtime, but applications usually
need numbers, booleans, URLs, enums, optional values, defaults, and clear startup
errors. env-typecheck lets you describe your environment once with Zod and then
use the parsed result with full TypeScript inference.
Benefits:
- Type-safe env access from your Zod schema.
- Runtime validation before the app starts serving traffic.
- Automatic coercion through Zod, such as
"3000"to3000. - Helpful formatted error messages for missing or invalid variables.
- Works with one
.envfile or multiple.envfiles. - Lightweight core API for Express and plain TypeScript projects.
- Optional NestJS module with dependency injection support.
- ESM and CommonJS builds.
- Sensitive values are redacted in NestJS validation logs by default.
Installation
npm install @alireza_ghasemi/env-typecheck zod dotenvzod and dotenv are regular dependencies of this package, so installing
@alireza_ghasemi/env-typecheck is enough for most npm setups. They are shown above because most
projects also import z directly in their own schema files.
For NestJS usage, your application should already have Nest installed:
npm install @nestjs/common @nestjs/core reflect-metadata rxjsQuick Start
import { createEnv, z } from "@alireza_ghasemi/env-typecheck";
export const env = createEnv(
z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
ENABLE_JOBS: z.coerce.boolean().default(false),
}),
);
env.PORT; // number
env.DATABASE_URL; // string
env.ENABLE_JOBS; // booleanIf validation fails, the process exits by default with a readable error message.
[env-typecheck] Invalid environment variables:
- DATABASE_URL: Invalid urlRecommended Project Setup
Create a small env file and import it wherever configuration is needed.
// src/env.ts
import { createEnv, z } from "@alireza_ghasemi/env-typecheck";
export const envSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
export const env = createEnv(envSchema);
export type Env = z.infer<typeof envSchema>;Then use the parsed config:
// src/index.ts
import { env } from "./env";
console.log(`Starting server on port ${env.PORT}`);Express Usage
See the runnable example in examples/express.
import express from "express";
import { createEnv, z } from "@alireza_ghasemi/env-typecheck";
const env = createEnv(
z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
CORS_ORIGIN: z.string().url().optional(),
}),
);
const app = express();
app.get("/health", (_req, res) => {
res.json({
ok: true,
env: env.NODE_ENV,
});
});
app.listen(env.PORT, () => {
console.log(`Server is running on port ${env.PORT}`);
});Because env.PORT is inferred as a number, you do not need to manually parse
it later in your application.
NestJS Usage
NestJS support is exported from the @alireza_ghasemi/env-typecheck/nestjs subpath.
See the runnable example in examples/nestjs.
// src/env.schema.ts
import { z } from "@alireza_ghasemi/env-typecheck";
export const envSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});Register the module in your root module:
// src/app.module.ts
import { Module } from "@nestjs/common";
import { EnvModule } from "@alireza_ghasemi/env-typecheck/nestjs";
import { envSchema } from "./env.schema";
@Module({
imports: [
EnvModule.forRoot({
isGlobal: true,
schema: envSchema,
envFilePath: [".env.local", ".env"],
}),
],
})
export class AppModule {}Inject the typed service:
// src/app.service.ts
import { Injectable } from "@nestjs/common";
import { EnvService } from "@alireza_ghasemi/env-typecheck/nestjs";
import { envSchema } from "./env.schema";
@Injectable()
export class AppService {
constructor(private readonly env: EnvService<typeof envSchema>) {}
getDatabaseUrl() {
return this.env.get("DATABASE_URL");
}
getPort() {
return this.env.get("PORT");
}
}Useful EnvService helpers:
this.env.get("PORT"); // typed value
this.env.getOrDefault("PORT", 3000); // fallback only when value is undefined
this.env.getAll(); // shallow copy of the parsed config
this.env.isDevelopment; // NODE_ENV === "development"
this.env.isProduction; // NODE_ENV === "production"
this.env.isTest; // NODE_ENV === "test"NestJS Async Registration
Use forRootAsync when the env file path depends on another provider or an async
operation.
import { Module } from "@nestjs/common";
import { EnvModule } from "@alireza_ghasemi/env-typecheck/nestjs";
import { envSchema } from "./env.schema";
@Module({
imports: [
EnvModule.forRootAsync({
isGlobal: true,
schema: envSchema,
useFactory: async () => {
return {
envFilePath:
process.env.NODE_ENV === "production" ? ".env.production" : ".env",
};
},
}),
],
})
export class AppModule {}Injecting the Raw Config in NestJS
You can also inject the parsed object directly.
import { Inject, Injectable } from "@nestjs/common";
import { ENV_CONFIG, EnvConfig } from "@alireza_ghasemi/env-typecheck/nestjs";
import { envSchema } from "./env.schema";
@Injectable()
export class DatabaseService {
constructor(
@Inject(ENV_CONFIG)
private readonly env: EnvConfig<typeof envSchema>,
) {}
}API
createEnv(schema, options?)
Validates process.env with a Zod object schema and returns the parsed result.
const env = createEnv(schema, {
path: ".env",
exitOnError: true,
});Options:
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| path | string | string[] | ".env" | One or more dotenv files to load before validation. |
| exitOnError | boolean | true | When true, validation errors are printed and the process exits with code 1. When false, an error is thrown. |
EnvModule.forRoot(options)
Registers the NestJS module synchronously.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| schema | ZodObject | required | Zod schema used to validate process.env. |
| envFilePath | string | string[] | ".env" | One or more dotenv files to load. |
| isGlobal | boolean | false | Makes the module global in NestJS. |
| onError | "exit" | "throw" | "exit" | Controls validation failure behavior. |
| redactKeys | string[] | ["PASSWORD", "SECRET", "KEY", "TOKEN", "PRIVATE"] | Key patterns hidden in success logs. |
EnvModule.forRootAsync(options)
Registers the NestJS module with an async factory.
EnvModule.forRootAsync({
schema,
inject: [],
useFactory: async () => ({
envFilePath: ".env",
}),
});EnvService<TSchema>
NestJS injectable wrapper around the parsed config.
| Method / getter | Description |
| --- | --- |
| get(key) | Returns a typed value from the parsed config. |
| getOrDefault(key, defaultValue) | Returns the value or a fallback when the value is undefined. |
| getAll() | Returns a shallow copy of all parsed config values. |
| isDevelopment | true when NODE_ENV is "development". |
| isProduction | true when NODE_ENV is "production". |
| isTest | true when NODE_ENV is "test". |
Multiple .env Files
Both the core API and NestJS module support multiple dotenv files.
const env = createEnv(schema, {
path: [".env.local", ".env"],
});EnvModule.forRoot({
schema,
envFilePath: [".env.local", ".env"],
});Files are loaded in the order you provide. Existing environment variables are
not overwritten because dotenv is loaded with override: false.
Type Inference
Types come directly from the schema.
const schema = z.object({
PORT: z.coerce.number(),
NODE_ENV: z.enum(["development", "production"]),
});
const env = createEnv(schema);
env.PORT.toFixed(0); // OK, PORT is number
env.NODE_ENV; // "development" | "production"Common Patterns
Required Secret
JWT_SECRET: z.string().min(32)Optional Value
SENTRY_DSN: z.string().url().optional()Default Number
PORT: z.coerce.number().int().positive().default(3000)Boolean Flag
ENABLE_JOBS: z.coerce.boolean().default(false)Comma-separated List
ALLOWED_ORIGINS: z
.string()
.transform((value) => value.split(",").map((item) => item.trim()))
.default("http://localhost:3000")Security Notes
- Do not commit real
.envfiles. - Keep
.env.examplein your repository with safe placeholder values. - Validate secrets at startup so broken deployments fail early.
- In NestJS, validation success logs redact keys containing
PASSWORD,SECRET,KEY,TOKEN, orPRIVATEby default. - Customize
redactKeysif your project uses different secret naming.
Publishing Checklist
Before publishing a new version:
npm run typecheck
npm test
npm run build
npm audit
npm pack --dry-runThe package exports:
import { createEnv, z } from "@alireza_ghasemi/env-typecheck";
import { EnvModule, EnvService } from "@alireza_ghasemi/env-typecheck/nestjs";License
MIT
