Next.js Env Variables
Next.js has built-in support for environment variables with special conventions.
Variable Prefixes
| Prefix | Available In | Use For |
|---|---|---|
NEXT_PUBLIC_ | Browser + Server | Public config, API URLs |
| No prefix | Server only | Secrets, database URLs |
Build-Time vs Runtime
Build-Time (Default)
Variables are inlined at build time:
// This gets replaced with actual value during build
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// After build, the code becomes:
const apiUrl = "https://api.example.com";Pros: Fast, no runtime lookup Cons: Need to rebuild to change values
Runtime Variables
For dynamic values that change without rebuilding:
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
// Server runtime config (deprecated, use env vars directly)
serverRuntimeConfig: {
apiSecret: process.env.API_SECRET,
},
// Public runtime config
publicRuntimeConfig: {
apiUrl: process.env.NEXT_PUBLIC_API_URL,
},
};
export default nextConfig;// Using runtime config
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
const apiUrl = publicRuntimeConfig.apiUrl;App Router Environment Variables
In Server Components
// app/page.tsx (Server Component)
async function Page() {
// All env vars available
const apiKey = process.env.API_KEY;
const data = await fetch('https://api.example.com', {
headers: { Authorization: `Bearer ${apiKey}` },
});
return <div>{/* ... */}</div>;
}In Client Components
// app/components/analytics.tsx
'use client';
function Analytics() {
// Only NEXT_PUBLIC_ vars available
const trackingId = process.env.NEXT_PUBLIC_GA_ID;
return <Script src={`https://www.googletagmanager.com/gtag/js?id=${trackingId}`} />;
}In Route Handlers
// app/api/users/route.ts
export async function GET() {
// All env vars available
const dbUrl = process.env.DATABASE_URL;
const users = await db.query('SELECT * FROM users');
return Response.json(users);
}In Middleware
// middleware.ts
export function middleware(request: NextRequest) {
// All env vars available
const apiKey = process.env.API_KEY;
// ...
}Configuration Pattern
// lib/config.ts
export const config = {
app: {
name: process.env.NEXT_PUBLIC_APP_NAME || 'MyApp',
url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
env: process.env.NODE_ENV,
},
api: {
url: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
timeout: 10000,
},
features: {
analytics: process.env.NEXT_PUBLIC_ENABLE_ANALYTICS === 'true',
debug: process.env.NODE_ENV === 'development',
},
} as const;
// Usage
import { config } from '@/lib/config';
console.log(config.app.name);
console.log(config.features.analytics);Server-Only Configuration
// lib/server-config.ts
import 'server-only'; // Ensures this only runs on server
export const serverConfig = {
database: {
url: process.env.DATABASE_URL!,
poolSize: parseInt(process.env.DB_POOL_SIZE || '10'),
},
auth: {
secret: process.env.NEXTAUTH_SECRET!,
jwtSecret: process.env.JWT_SECRET!,
},
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
},
};Dynamic Environment Detection
// lib/env.ts
export function getBaseUrl() {
// Browser
if (typeof window !== 'undefined') {
return '';
}
// Vercel
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
}
// Other platforms
if (process.env.NEXT_PUBLIC_APP_URL) {
return process.env.NEXT_PUBLIC_APP_URL;
}
// Localhost
return `http://localhost:${process.env.PORT || 3000}`;
}
// For absolute URLs
export function getAbsoluteUrl(path: string) {
return `${getBaseUrl()}${path}`;
}Environment-Specific Behavior
// lib/env.ts
export const isDevelopment = process.env.NODE_ENV === 'development';
export const isProduction = process.env.NODE_ENV === 'production';
export const isTest = process.env.NODE_ENV === 'test';
// Vercel-specific
export const isPreview = process.env.VERCEL_ENV === 'preview';
export const isVercel = !!process.env.VERCEL;
// Usage
if (isDevelopment) {
console.log('Debug info:', data);
}
if (isPreview) {
// Show preview banner
}Vercel Environment Variables
Vercel provides automatic environment variables:
# Automatically set by Vercel
VERCEL=1
VERCEL_ENV=production|preview|development
VERCEL_URL=your-site-xxx.vercel.app
VERCEL_GIT_COMMIT_SHA=abc123
VERCEL_GIT_COMMIT_MESSAGE=feat: add feature
VERCEL_GIT_REPO_SLUG=my-repo// Use for preview deployments
function PreviewBanner() {
if (process.env.VERCEL_ENV !== 'preview') {
return null;
}
return (
<div className="bg-yellow-500 p-2 text-center">
Preview: {process.env.VERCEL_GIT_COMMIT_MESSAGE}
</div>
);
}Loading External Config
// next.config.mjs
import { config } from 'dotenv';
// Load from custom file
config({ path: '.env.custom' });
/** @type {import('next').NextConfig} */
const nextConfig = {
env: {
CUSTOM_VAR: process.env.CUSTOM_VAR,
},
};
export default nextConfig;Edge Runtime Variables
// app/api/edge/route.ts
export const runtime = 'edge';
export async function GET() {
// Environment variables work in Edge runtime
const apiKey = process.env.API_KEY;
return Response.json({ status: 'ok' });
}Testing with Environment Variables
// jest.setup.js
process.env.NEXT_PUBLIC_API_URL = 'http://localhost:8000';
process.env.DATABASE_URL = 'postgresql://localhost:5432/test';
// Or use .env.test file// __tests__/api.test.ts
describe('API', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
});
afterAll(() => {
process.env = originalEnv;
});
it('uses correct API URL', () => {
process.env.NEXT_PUBLIC_API_URL = 'http://test-api.com';
// Test...
});
});Best Practices
- Always use NEXT_PUBLIC_ for client vars - Security requirement
- Centralize config - Create a config file, don't scatter process.env calls
- Validate on startup - Fail fast with clear error messages
- Use TypeScript - Define types for process.env
- Document variables - Maintain .env.example with all required vars