node-env-resolver-nextjs
v7.3.0
Published
Zero-config Next.js integration for node-env-resolver with client/server split and App Router support
Maintainers
Readme
node-env-resolver-nextjs
Next.js integration with automatic client/server environment variable splitting.
Install
npm install node-env-resolver-nextjsQuick start
Create env.mjs in your project root:
import { resolve } from 'node-env-resolver-nextjs';
export const env = resolve({
server: {
DATABASE_URL: url(),
RESEND_API_KEY: string(),
PORT: 'port:3000',
},
client: {
NEXT_PUBLIC_APP_URL: url(),
NEXT_PUBLIC_GA_ID: string({optional:true}),
}
});Use in your app:
// Server component or API route
import { env } from '../env.mjs';
console.log(env.server.DATABASE_URL);
console.log(env.client.NEXT_PUBLIC_APP_URL);// Client component
'use client';
import { env } from '../env.mjs';
console.log(env.client.NEXT_PUBLIC_GA_ID); // ✓ Works
console.log(env.server.DATABASE_URL); // ✗ Throws errorSafe resolve (Zod-like pattern)
Like Zod's parse() vs safeParse(), you can choose between throwing errors or getting result objects:
import { resolve, safeResolve } from 'node-env-resolver/nextjs';
// ❌ Throws on validation failure (like Zod's parse())
export const env = resolve({
server: {
DATABASE_URL: url(),
API_SECRET: string(),
},
client: {
NEXT_PUBLIC_APP_URL: url(),
}
});
// ✅ Returns result object (like Zod's safeParse())
const result = safeResolve({
server: {
DATABASE_URL: url(),
API_SECRET: string(),
},
client: {
NEXT_PUBLIC_APP_URL: url(),
}
});
if (result.success) {
export const env = result.data;
// env.server and env.client are fully typed
} else {
console.error('Environment validation failed:', result.error);
process.exit(1);
}Use cases:
resolve()- Throws on error (simpler, fail-fast)safeResolve()- Returns{ success, data?, error? }(graceful error handling)
Features
- Automatic client/server split
- Runtime protection to prevent leaking server vars to client
- Full TypeScript support
- Works with Next.js 13+ App Router
- Zero configuration required
- Supports all validator types (basic and advanced)
Environment files
The package automatically loads from Next.js standard env files:
.env.defaults # Shared defaults
.env # Shared variables
.env.local # Local overrides (gitignored)
.env.development # Development
.env.production # ProductionExample .env.local:
# Server-only (no NEXT_PUBLIC_ prefix)
DATABASE_URL=postgres://localhost:5432/myapp
RESEND_API_KEY=re_abc123
# Client-accessible (NEXT_PUBLIC_ prefix required)
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_GA_ID=GA-123456Validation
Use the same shorthand syntax as the core package:
export const env = resolve({
server: {
PORT: 'port:3000',
DATABASE_URL: url(),
NODE_ENV: ['development', 'test', 'production'] as const,
API_KEY: string(),
MAX_CONNECTIONS: { type: 'number', min: 1, max: 100 },
},
client: {
NEXT_PUBLIC_API_URL: url(),
NEXT_PUBLIC_ENABLE_ANALYTICS: false,
NEXT_PUBLIC_GA_ID: string({optional:true}),
}
});TypeScript knows all the types:
env.server.PORT; // number
env.server.DATABASE_URL; // URL
env.server.NODE_ENV; // 'development' | 'test' | 'production'
env.server.MAX_CONNECTIONS; // number
env.client.NEXT_PUBLIC_API_URL; // URL
env.client.NEXT_PUBLIC_GA_ID; // string | undefinedRuntime protection
The package prevents server variables from being accessed in client code:
'use client';
import { env } from './env.mjs';
console.log(env.server.DATABASE_URL);
// Error: Cannot access server environment variable 'DATABASE_URL' in client-side code.
// Server variables are only available in server components, API routes, and middleware.This protection works in both development and production.
Options
export const env = resolve({
server: { /* ... */ },
client: { /* ... */ }
}, {
clientPrefix: 'NEXT_PUBLIC_', // Default prefix
runtimeProtection: true, // Enable runtime checks (default: true)
expandVars: true, // Enable ${VAR} expansion (default: true)
});Custom resolvers
Note: Custom resolvers are not yet supported in the Next.js integration because Next.js config files must be synchronous, and most resolvers (AWS Secrets, Vault, etc.) are async.
If you need custom resolvers, use the core node-env-resolver package in your API routes or server components instead:
// app/api/config/route.ts
import { resolve } from 'node-env-resolver';
import { awsSecrets } from 'node-env-resolver-aws';
// This is async and works in API routes
export async function GET() {
const config = await resolveAsync({
resolvers: [
[awsSecrets({ secretId: 'prod/app/secrets' }), {
DATABASE_URL: url(),
API_KEY: string()
}]
]
});
return Response.json({ status: 'ok' });
}Production security
In production, .env files are automatically ignored. Production platforms (Vercel, AWS) inject variables via process.env.
export const env = resolve({
server: {
DATABASE_URL: url(),
API_SECRET: string(),
PORT: 'port:3000',
},
client: {
NEXT_PUBLIC_APP_URL: url(),
}
});Production platforms like Vercel automatically inject environment variables into process.env, so no additional configuration is needed.
Example: SaaS app
import { resolve } from 'node-env-resolver-nextjs';
export const env = resolve({
server: {
DATABASE_URL: url(),
NEXTAUTH_SECRET: string(),
NEXTAUTH_URL: url(),
STRIPE_SECRET_KEY: string(),
RESEND_API_KEY: string(),
PORT: 'port:3000',
NODE_ENV: ['development', 'test', 'production'] as const,
},
client: {
NEXT_PUBLIC_APP_URL: url(),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: string(),
NEXT_PUBLIC_POSTHOG_KEY: string({optional:true}),
NEXT_PUBLIC_ENABLE_ANALYTICS: false,
}
});Troubleshooting
Variables not loading?
- Check file names and locations
- Ensure client variables have
NEXT_PUBLIC_prefix - Restart dev server after adding new variables
TypeScript errors?
- Verify schema matches
.envfiles - Restart TypeScript server in your editor
Production issues?
- Check platform environment variables are set
- Ensure secrets aren't sourced from
.envfiles - Verify client variables are properly prefixed
License
MIT
