DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Type-Safe Environment Variables in Node.js With Zod

Type-Safe Environment Variables in Node.js With Zod

Missing environment variables crash apps in production. Undocumented variables create confusion. Here's how to validate and type your env vars at startup.

The Problem

// This compiles fine but crashes at runtime if DATABASE_URL is missing
const db = new PrismaClient({ url: process.env.DATABASE_URL });
// TypeError: Cannot read property of undefined
Enter fullscreen mode Exit fullscreen mode

Validate With Zod at Startup

// env.ts — load this before anything else
import { z } from 'zod';

const envSchema = z.object({
  // Database
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),

  // Auth
  NEXTAUTH_SECRET: z.string().min(32),
  NEXTAUTH_URL: z.string().url(),

  // Stripe
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),

  // App
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().default(3000),

  // Optional
  SENTRY_DSN: z.string().url().optional(),
});

const result = envSchema.safeParse(process.env);

if (!result.success) {
  console.error('Invalid environment variables:');
  console.error(result.error.flatten().fieldErrors);
  process.exit(1); // Fail fast — don't start with invalid config
}

export const env = result.data;
// env is fully typed: env.PORT is number, env.DATABASE_URL is string
Enter fullscreen mode Exit fullscreen mode

Using the Typed Env

import { env } from './env';

const db = new PrismaClient({ url: env.DATABASE_URL }); // typed, validated
const port = env.PORT; // number, not string
Enter fullscreen mode Exit fullscreen mode

T3 Env: The Library Version

npm install @t3-oss/env-nextjs
Enter fullscreen mode Exit fullscreen mode
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    STRIPE_SECRET_KEY: z.string(),
  },
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
});
Enter fullscreen mode Exit fullscreen mode

.env.example as Documentation

# .env.example — commit this, not .env
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
REDIS_URL=redis://localhost:6379
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
Enter fullscreen mode Exit fullscreen mode

Type-safe env validation, a complete .env.example, and startup checks are pre-configured in the AI SaaS Starter Kit.

Top comments (0)