27 Environment Variables in Next.js
27.1 Overview
27.1.1 Basic Structure
Next.js uses .env*
files at your project root:
your-nextjs-project/
├── .env.local # Local overrides (gitignored)
├── .env.development # Development environment
├── .env.production # Production environment
└── .env # Default for all environments
Loading Priority (highest to lowest):
.env.local → .env.development/.env.production → .env
27.1.2 Two Types of Variables
27.1.2.1 A. Server-Side Only (Default)
Variables are private by default - only accessible in Node.js environment:
# .env.local
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET_KEY=super_secret_key
// pages/api/users.ts or app/api/users/route.ts
export default function handler(req, res) {
const dbUrl = process.env.DATABASE_URL; // ✅ Works
// Never exposed to browser
}
27.1.2.2 B. Browser-Exposed Variables
Prefix with NEXT_PUBLIC_
to expose to the browser:
# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
// components/Analytics.tsx (client component)
'use client'
export default function Analytics() {
const gaId = process.env.NEXT_PUBLIC_API_URL; // ✅ Works in browser
return <div>API: {gaId}</div>
}
27.1.3 Important Concepts
27.1.3.1 Build-Time vs Runtime
Development: Production:
┌──────────────┐ ┌──────────────┐
│ npm run dev │ │ npm run build│
│ │ │ │
│ .env.local │ │ .env.production
│ loaded on │ │ baked into │
│ every reload │ │ bundle │
└──────────────┘ └──────────────┘
↓
┌──────────────┐
│ npm start │
│ │
│ uses built │
│ bundle │
└──────────────┘
Key Point: In production, env vars are baked into the bundle at build time. To change them, you must rebuild!
27.1.4 TypeScript Type Safety
Coming from Dart’s strong typing, you’ll appreciate this:
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
: string;
DATABASE_URL: string;
NEXT_PUBLIC_API_URL: 'development' | 'production' | 'test';
NODE_ENV
}
}
// Now you get autocomplete and type checking!
const dbUrl: string = process.env.DATABASE_URL;
27.1.5 Common Pitfalls (Watch Out!)
27.1.5.1 ❌ Dynamic Access Won’t Work
// ❌ Won't work - Next.js can't statically analyze this
const key = 'API_KEY';
const value = process.env[key];
// ✅ Works - direct access
const value = process.env.API_KEY;
27.1.5.2 ❌ Client Component Secrets
'use client'
// ❌ This will be undefined in browser!
const secret = process.env.DATABASE_URL;
// ✅ This works - has NEXT_PUBLIC_ prefix
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
27.1.6 Practical Example
# .env.local
DATABASE_URL=postgresql://localhost:5432/radiology_db
OPENAI_API_KEY=sk-xxxxx
NEXT_PUBLIC_APP_URL=http://localhost:3000
// app/api/analyze-image/route.ts (Server)
export async function POST(req: Request) {
const db = process.env.DATABASE_URL; // ✅ Server-side
const apiKey = process.env.OPENAI_API_KEY; // ✅ Server-side
// Process medical image...
}
// components/UploadForm.tsx (Client)
'use client'
export default function UploadForm() {
const appUrl = process.env.NEXT_PUBLIC_APP_URL; // ✅ Browser
const handleUpload = async (file: File) => {
await fetch(`${appUrl}/api/analyze-image`, {
: 'POST',
method: file
body;
});
} }
27.1.7 Comparison with Flutter
Aspect | Flutter/Dart | Next.js |
---|---|---|
Config Files | Multiple approaches (dart-define, json, packages) | Standard .env* files |
Build-time | --dart-define at compile |
Baked into bundle |
Public/Private | Manual separation | Automatic with NEXT_PUBLIC_ |
Type Safety | Built-in with Dart | Manual declaration needed |
Quick Tips for Your Radiology AI Project: - Store database credentials without NEXT_PUBLIC_
- API endpoints that client needs → use NEXT_PUBLIC_
- Always add .env.local
to .gitignore
- For deployment (Netlify/Vercel), set env vars in their dashboard
27.2 Env from CLI
Q: Can Next JS support defining build-time env vars from the CLI?
Next.js supports this, though it works differently than Flutter’s --dart-define
. Let me show you the approaches:
27.2.1 Direct Inline Definition (Most Similar to --dart-define
)
# Single variable
NEXT_PUBLIC_API_URL=https://prod.api.com npm run build
# Multiple variables
DATABASE_URL=postgres://prod API_KEY=secret npm run build
# Compare with Flutter:
flutter build apk --dart-define=API_URL=https://api.com
Full Example
# Development build with custom API
NEXT_PUBLIC_API_URL=https://dev-api.hospital.com \
DATABASE_URL=postgresql://dev-db:5432/radiology \
npm run dev
# Production build with staging config
NODE_ENV=production \
NEXT_PUBLIC_API_URL=https://staging-api.hospital.com \
OPENAI_API_KEY=sk-staging-key \
npm run build
27.2.2 Using --env
Flag with dotenv-cli
(More Organized)
Install the package:
npm install --save-dev dotenv-cli
Create environment-specific files:
your-project/
├── .env.staging
├── .env.production
└── .env.development
Update package.json
:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"build:staging": "dotenv -e .env.staging -- next build",
"build:prod": "dotenv -e .env.production -- next build",
"build:custom": "dotenv -e .env.custom -- next build"
}
}
Usage:
npm run build:staging
npm run build:prod
# Or inline override:
dotenv -e .env.staging -- npm run build
27.2.3 Using cross-env
(Cross-Platform Compatible)
Great for your macOS + potential CI/CD on Linux:
npm install --save-dev cross-env
{
"scripts": {
"build:staging": "cross-env NEXT_PUBLIC_API_URL=https://staging.api.com npm run build",
"build:prod": "cross-env NEXT_PUBLIC_API_URL=https://prod.api.com npm run build",
"dev:mock": "cross-env NEXT_PUBLIC_USE_MOCK_DATA=true npm run dev"
}
}
27.2.4 Custom Build Script (Most Flexible)
Similar to how you might use shell scripts with Flutter:
// scripts/build.js
const { execSync } = require('child_process');
const configs = {
dev: {
NEXT_PUBLIC_API_URL: 'https://dev-api.hospital.com',
DATABASE_URL: 'postgresql://dev-db:5432/radiology'
,
}staging: {
NEXT_PUBLIC_API_URL: 'https://staging-api.hospital.com',
DATABASE_URL: 'postgresql://staging-db:5432/radiology'
,
}prod: {
NEXT_PUBLIC_API_URL: 'https://api.hospital.com',
DATABASE_URL: 'postgresql://prod-db:5432/radiology'
};
}
const env = process.argv[2] || 'dev';
const config = configs[env];
const envVars = Object.entries(config)
.map(([key, value]) => `${key}=${value}`)
.join(' ');
execSync(`${envVars} npm run build`, { stdio: 'inherit' });
{
"scripts": {
"build:custom": "node scripts/build.js",
"build:dev": "node scripts/build.js dev",
"build:staging": "node scripts/build.js staging",
"build:prod": "node scripts/build.js prod"
}
}
Usage:
npm run build:staging
npm run build:prod
27.2.5 Using .env
Files with CLI Override
The hybrid approach (most practical):
# .env.staging
NEXT_PUBLIC_API_URL=https://staging-api.hospital.com
DATABASE_URL=postgresql://staging-db:5432/radiology
LOG_LEVEL=debug
# Override specific variables from CLI
# (CLI takes highest priority)
NEXT_PUBLIC_API_URL=https://custom-api.com \
npm run build
# Or with dotenv-cli:
dotenv -e .env.staging -- \
\
env NEXT_PUBLIC_API_URL=https://override.com npm run build
27.2.6 Priority Chain with CLI
Environment Variable Priority:
────────────────────────────────────────────────
1. CLI inline variables ← HIGHEST
2. .env.local
3. .env.development/.env.production
4. .env ← LOWEST
Example:
# .env.production has:
NEXT_PUBLIC_API_URL=https://prod.api.com
# CLI override wins:
NEXT_PUBLIC_API_URL=https://staging.api.com npm run build
# → Uses https://staging.api.com ✅
27.2.7 Real-World: CI/CD Pipeline
For GitHub Actions (similar to AppVeyor you use):
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main, staging]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build for Staging
if: github.ref == 'refs/heads/staging'
env:
NEXT_PUBLIC_API_URL: ${{ secrets.STAGING_API_URL }}
DATABASE_URL: ${{ secrets.STAGING_DB_URL }}
run: npm run build
- name: Build for Production
if: github.ref == 'refs/heads/main'
env:
NEXT_PUBLIC_API_URL: ${{ secrets.PROD_API_URL }}
DATABASE_URL: ${{ secrets.PROD_DB_URL }}
run: npm run build
27.2.8 Comparison: Flutter vs Next.js
Flutter (Dart):
─────────────────────────────────────────────────
flutter build apk \
--dart-define=API_URL=https://api.com \
--dart-define=API_KEY=secret
Access in Dart:
const apiUrl = String.fromEnvironment('API_URL');
Next.js (JavaScript/TypeScript):
─────────────────────────────────────────────────
NEXT_PUBLIC_API_URL=https://api.com \
API_KEY=secret \
npm run build
Access in TypeScript:
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const apiKey = process.env.API_KEY; // server-only
27.2.9 Recommended Setup
// package.json
{
"scripts": {
"dev": "next dev",
"dev:local": "dotenv -e .env.local -- next dev",
"build": "next build",
"build:staging": "dotenv -e .env.staging -- next build",
"build:prod": "dotenv -e .env.production -- next build",
"deploy:netlify:staging": "npm run build:staging && netlify deploy",
"deploy:netlify:prod": "npm run build:prod && netlify deploy --prod"
}
}
Project structure:
radiology-ai-app/
├── .env.local # Your local development
├── .env.staging # Staging environment
├── .env.production # Production environment
├── .env.example # Template (committed)
├── scripts/
│ └── build-with-env.js # Custom build script if needed
└── package.json
27.2.10 Quick Commands Reference
# Development
npm run dev
NEXT_PUBLIC_API_URL=https://custom.com npm run dev
# Build
npm run build
NODE_ENV=production npm run build
# With multiple vars (macOS/Linux)
NEXT_PUBLIC_API_URL=https://api.com \
DATABASE_URL=postgresql://db:5432/rad \
LOG_LEVEL=debug \
npm run build
# With dotenv-cli
dotenv -e .env.staging -- npm run build
# With cross-env (Windows-compatible)
cross-env NEXT_PUBLIC_API_URL=https://api.com npm run build
TL;DR: - ✅ Yes, Next.js supports CLI env vars like --dart-define
- Use inline: VAR=value npm run build
- For organization: Use dotenv-cli
or custom scripts - CLI variables have highest priority, overriding .env
files