Skip to main content

Documentation Index

Fetch the complete documentation index at: https://next-safe-env.dev/llms.txt

Use this file to discover all available pages before exploring further.

If your project already uses Zod, you can pass z.object() schemas directly to createEnv instead of rewriting your validation logic using next-safe-env’s native validators. There is no peer dependency requirement - next-safe-env detects Zod schemas at runtime via structural typing (duck-typing), so installing next-safe-env will not pull Zod into projects that do not already have it.

Complete example

import { z } from 'zod'
import { createEnv } from 'next-safe-env'

export const env = createEnv({
  server: z.object({
    DATABASE_URL: z.string().url(),
    PORT:         z.coerce.number().int().min(1).max(65535).default(3000),
    NODE_ENV:     z.enum(['development', 'production', 'test']).default('development'),
  }),
  client: z.object({
    NEXT_PUBLIC_APP_NAME: z.string().default('My App'),
  }),
  runtimeEnv: {
    DATABASE_URL:         process.env.DATABASE_URL,
    PORT:                 process.env.PORT,
    NODE_ENV:             process.env.NODE_ENV,
    NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
  },
})

Mixing native and Zod schemas

You can use a Zod schema for one side of the split and native next-safe-env validators for the other. This is useful when you want to migrate incrementally or when the two schemas are maintained by different parts of your team:
import { z } from 'zod'
import { createEnv, str, url } from 'next-safe-env'

export const env = createEnv({
  server: z.object({
    DATABASE_URL: z.string().url(),
    NODE_ENV:     z.enum(['development', 'production', 'test']),
  }),
  client: {
    NEXT_PUBLIC_API_URL:  url(),
    NEXT_PUBLIC_APP_NAME: str().default('My App'),
  },
  runtimeEnv: {
    DATABASE_URL:         process.env.DATABASE_URL,
    NODE_ENV:             process.env.NODE_ENV,
    NEXT_PUBLIC_API_URL:  process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
  },
})

What works with Zod

All standard Zod features work end-to-end when passed through createEnv:
  • Coercions - z.coerce.number() parses the raw string value into a number, the same way num() does natively.
  • Transforms - z.string().transform(v => v.trim()) runs after parsing and its output type is reflected in the inferred return type.
  • Defaults - z.string().default('My App') is applied when the variable is undefined, the same as .default('My App') on a native validator.
  • Enums - z.enum(['development', 'production', 'test']) narrows the inferred type to the literal union.

Error output

When a Zod field fails validation, next-safe-env maps the Zod error into the same ValidationFailure shape it uses for native validator failures. All errors appear together in the same pretty-printed output:
[next-safe-env] Environment validation failed - 2 error(s):

  ✗ DATABASE_URL  - Required. Expected a valid URL. Got: "localhost"
  ✗ PORT          - Expected number, received nan

  Server vars:  1 valid, 2 invalid
  Client vars:  1 valid, 0 invalid

  Set the correct values in your .env file or deployment environment and restart.
You do not need to handle Zod errors separately - they surface through the same onValidationError callback and the same exit path as native validation failures.
All adapter rules still apply when you use Zod schemas. If you are using the nextjs adapter, every key in client must be prefixed with NEXT_PUBLIC_. The adapter enforces this rule against the schema keys regardless of whether the schema is native or Zod-based. Server vars are still stripped from the returned object in browser context.
You still need Zod installed in your own project - next-safe-env will not install it for you. The “no peer dependency” guarantee means next-safe-env will not break if you do not have Zod installed; it simply will not recognize non-Zod objects as Zod schemas.