next-safe-env
v1.0.0
Published
Typed, validated environment variables for Next.js and Node.js
Maintainers
Readme
The Problem
Every Next.js and Node.js project has the same boilerplate:
const DATABASE_URL = process.env.DATABASE_URL
if (!DATABASE_URL) throw new Error('Missing DATABASE_URL')
const PORT = parseInt(process.env.PORT ?? '3000', 10)
if (isNaN(PORT)) throw new Error('PORT must be a number')process.env.X is always string | undefined - no types, no autocomplete. Missing or malformed vars surface mid-request, not at startup. Nothing stops you from reading a server secret in a client component and getting a silent undefined in the browser. Every project re-writes the same validation logic with no single place to audit what the app needs to run.
next-safe-env fixes all of this with a single function call.
Why next-safe-env?
| Feature | next-safe-env | t3-env | envalid | dotenv + Zod |
|---|:---:|:---:|:---:|:---:|
| Zero dependencies | ✅ | ❌ | ❌ | ❌ |
| Next.js App Router support | ✅ | Partial | ❌ | ❌ |
| Server/client TypeScript split | ✅ | ✅ | ❌ | Manual |
| Edge Runtime adapter | ✅ | ❌ | ❌ | ❌ |
| Fluent validator API | ✅ | Schema-based | Custom | Schema-based |
| Pretty error output | ✅ | Partial | ✅ | Manual |
| Bundle size | < 5 kB | ~50 kB+ | ~10 kB | ~50 kB+ |
| Auto-enforce NEXT_PUBLIC_ prefix | ✅ | Manual | ❌ | ❌ |
| Zod interop | Optional ✅ | Required | ❌ | Required |
| ClientEnv<T> server-only branding | ✅ | Partial | ❌ | Manual |
| Vite adapter | ✅ | ✅ | ❌ | Manual |
| CLI (check / init) | ✅ | ❌ | ❌ | ❌ |
If your project already uses a schema validation library, tools like t3-env or envalid integrate well with your existing setup. next-safe-env is for teams that want typed, validated env vars with no additional dependencies - the full feature set ships in under 5 kB.
Requirements
- Node.js 18+
- TypeScript 5.x
No runtime dependencies.
Installation
npm install next-safe-env
# or
pnpm add next-safe-env
# or
yarn add next-safe-env
next-safe-envvalidates what is already inprocess.env. It does not load.envfiles. For that, use Next.js's built-in.envsupport ordotenv.
Quick Start
// src/env.ts
import { createEnv, str, url, port, bool } from 'next-safe-env'
export const env = createEnv({
server: {
DATABASE_URL: url(), // must be a valid URL
PORT: port().default(3000), // coerced to number, defaults to 3000
NODE_ENV: str().enum(['development', 'production', 'test']),
},
client: {
NEXT_PUBLIC_APP_NAME: str().default('My App'),
NEXT_PUBLIC_ENABLE_DEBUG: bool().default(false),
},
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,
NEXT_PUBLIC_ENABLE_DEBUG: process.env.NEXT_PUBLIC_ENABLE_DEBUG,
},
})// anywhere in your app
import { env } from '@/env'
env.DATABASE_URL // string
env.PORT // number - not string
env.NEXT_PUBLIC_APP_NAME // stringIf any variable is missing or invalid, the app refuses to start and prints every problem at once:
[next-safe-env] Environment validation failed - 3 error(s):
✗ DATABASE_URL - Required. Expected a valid URL. Got: "postgres-localhost"
✗ JWT_SECRET - Too short. Must be ≥ 32 characters. Got length: 12
✗ SMTP_PORT - Invalid port. Must be 1–65535. Got: "99999"CLI
next-safe-env ships a zero-install CLI for validation and scaffolding.
check — validate before you deploy
Imports your compiled env file in an isolated process and exits 0 if all vars are valid, 1 if any fail. Drop it into any CI pipeline to gate deployments:
# Auto-discovers src/env.js then dist/env.js
npx next-safe-env check
# Or point at a specific file
npx next-safe-env check ./dist/env.js[next-safe-env] Checking src/env.js...
[next-safe-env] Environment validation failed — 2 error(s):
✗ DATABASE_URL — Expected valid URL. Got: "postgres-localhost"
✗ JWT_SECRET — Expected length >= 32. Got length: 12
[next-safe-env] ✗ Validation failed.init — generate src/env.ts and .env.example
An interactive scaffold that asks which variables your app needs, their types, defaults, and constraints — then writes a ready-to-use env.ts and a commented .env.example:
npx next-safe-env init
# Custom output path
npx next-safe-env init --output config/env.tsThe generated .env.example includes inline comments for every variable so new contributors know exactly what to fill in:
# DATABASE_URL — required valid URL
DATABASE_URL=
# PORT — required port number (1–65535)
# Default: 3000
PORT=3000
# NODE_ENV — required string
# Allowed values: development | production | test
NODE_ENV=Documentation
The full documentation is available at next-safe-env.dev.
Guides
- Getting Started - Install and validate your first env var in minutes
- Next.js App Router - Server/client splitting with automatic
NEXT_PUBLIC_enforcement - Node.js - Plain Node.js servers, APIs, and CLI scripts
- Edge Runtime - Vercel Edge Runtime and Next.js Middleware
- Vite - Non-Next.js React apps with
import.meta.env - Zod Interop - Pass
z.object(...)schemas directly, no rewrites needed - Testing - Skip validation in test environments without removing your schema
- CLI -
checkandinitcommands for CI validation and interactive scaffolding
API Reference
- Validators -
str,num,bool,url,portand their chainable rules - createEnv() - Full reference for every configuration option
- TypeScript Types -
ServerOnly<T>,ClientEnv<T>, and all exported types
Concepts
- Adapters - How Next.js, Node.js, Edge Runtime, and Vite adapters work
- Server & Client Split - How env vars are separated and protected per runtime context
- Error Handling - Validation errors, pretty output, and custom error handlers
next-safe-env was built because process.env.X should never be string | undefined in a typed codebase - and getting full validation, type inference, and Next.js adapter support shouldn't require adding new dependencies to do it.
MIT © 2026
