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 {
    DATABASE_URL: string;
    NEXT_PUBLIC_API_URL: string;
    NODE_ENV: 'development' | 'production' | 'test';
  }
}

// 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`, {
      method: 'POST',
      body: file
    });
  };
}

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

Note

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.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