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.

next-safe-env works equally well outside Next.js. In any Express server, Fastify app, CLI script, or background worker, you get the same validated, fully-typed env object with no framework-specific setup. Numeric variables like PORT come back as actual numbers, boolean flags are coerced from strings, and the process refuses to start if anything is misconfigured.
1

Install next-safe-env

Add the package to your project. It has zero runtime dependencies.
npm install next-safe-env
2

Create config/env.ts

Define your schema in a dedicated config file. Set adapter: 'node' explicitly, or omit it and rely on auto-detection - when NEXT_RUNTIME is not set and no NEXT_PUBLIC_ keys are present, next-safe-env selects the node adapter automatically.
// config/env.ts
import { createEnv, str, bool, url, port } from 'next-safe-env'

export const env = createEnv({
  server: {
    DATABASE_URL:  url(),
    PORT:          port().default(3000),
    LOG_LEVEL:     str().enum(['debug', 'info', 'warn', 'error']).default('info'),
    ENABLE_CACHE:  bool().default(true),
    REDIS_URL:     url().optional(),
  },
  client: {},
  runtimeEnv: {
    DATABASE_URL:  process.env.DATABASE_URL,
    PORT:          process.env.PORT,
    LOG_LEVEL:     process.env.LOG_LEVEL,
    ENABLE_CACHE:  process.env.ENABLE_CACHE,
    REDIS_URL:     process.env.REDIS_URL,
  },
  adapter: 'node',
})
next-safe-env validates what is already in process.env. It does not load .env files. Use dotenv or your platform’s native secret injection to populate process.env before createEnv runs.
3

Use it in your server

Import env anywhere in your application. Numeric vars like PORT are already coerced to number - no parseInt needed.
// server.ts
import { env } from './config/env.js'

app.listen(env.PORT, () => {
  console.log(`Listening on port ${env.PORT}`)  // number, not string
})
Every other variable is available with its correct inferred type:
import { env } from './config/env.js'

env.DATABASE_URL  // string
env.PORT          // number
env.LOG_LEVEL     // 'debug' | 'info' | 'warn' | 'error'
env.ENABLE_CACHE  // boolean
env.REDIS_URL     // string | undefined

The node adapter

The node adapter is intentionally minimal. Both of its hooks - beforeValidate and afterValidate - are no-ops. Raw env values go in, validated and coerced values come out unchanged. There are no prefix rules, no key-stripping, and no browser-context checks. Every var from both server and client schemas is present in the returned object. This makes the node adapter appropriate for any environment where you control the full process and want all variables available without restriction.

Auto-detection

When you omit adapter, next-safe-env selects the adapter based on the runtime environment:
ConditionAdapter selected
process is undefined or has no versionedge
process.env.NEXT_RUNTIME is setnextjs
Otherwisenode
In a plain Node.js process where NEXT_RUNTIME is not set, node is always selected. Setting adapter: 'node' explicitly is recommended for clarity and to make your intent self-documenting.

What happens when validation fails

If any variable is missing or invalid, the process exits before serving a single request and prints every problem at once:
[next-safe-env] Environment validation failed - 2 error(s):

  ✗ DATABASE_URL     - Required. Expected a valid URL. Got: undefined
  ✗ PORT             - Invalid port. Must be 1–65535. Got: "99999"

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

  Set the correct values in your .env file or deployment environment and restart.