Skip to content

Fix: Next.js Environment Variables Returning undefined

FixDevs ·

Quick Answer

How to fix Next.js environment variables returning undefined — NEXT_PUBLIC prefix rules, server vs client context, .env file loading order, and runtime vs build-time variable access.

The Error

You define environment variables in .env.local but they come back as undefined in your Next.js app:

console.log(process.env.API_KEY);        // undefined
console.log(process.env.DATABASE_URL);   // undefined

Or a variable is defined but only works on the server:

// In a client component
console.log(process.env.NEXT_PUBLIC_API_URL); // undefined in the browser

Or variables work in development but are undefined after deployment.

Why This Happens

Next.js has a strict system for environment variables with rules that differ from plain Node.js:

  • Client-side variables must be prefixed with NEXT_PUBLIC_ — without this prefix, variables are server-only and are never sent to the browser bundle.
  • Variables are inlined at build time — Next.js replaces process.env.NEXT_PUBLIC_* references with their literal values during the build. If the variable was not set at build time, it is undefined forever.
  • .env files are loaded in a specific order.env.local overrides .env, .env.development.local overrides .env.development, etc.
  • Server components vs client components — in Next.js 13+ App Router, server components have access to all env vars; client components only have access to NEXT_PUBLIC_* vars.
  • Runtime environment vs build environment — on Vercel and similar platforms, env vars set in the dashboard are injected at build time, not runtime (for client vars).

Fix 1: Add NEXT_PUBLIC_ Prefix for Client-Side Variables

Any environment variable you need in browser-executed code (components, hooks, client-side API calls) must start with NEXT_PUBLIC_:

Broken — missing prefix:

# .env.local
API_URL=https://api.example.com
STRIPE_PUBLISHABLE_KEY=pk_test_abc123
// In a React component (client-side)
const url = process.env.API_URL;              // undefined
const key = process.env.STRIPE_PUBLISHABLE_KEY; // undefined

Fixed — add NEXT_PUBLIC_ prefix:

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_STRIPE_KEY=pk_test_abc123

# Server-only vars (no prefix needed — keep secrets here)
DATABASE_URL=postgres://user:pass@localhost/db
STRIPE_SECRET_KEY=sk_test_xyz789
// In a React component
const url = process.env.NEXT_PUBLIC_API_URL;  // "https://api.example.com"
const key = process.env.NEXT_PUBLIC_STRIPE_KEY; // "pk_test_abc123"

Warning: Never prefix secret keys (DATABASE_URL, STRIPE_SECRET_KEY, JWT_SECRET) with NEXT_PUBLIC_. They will be embedded in your JavaScript bundle and visible to anyone who inspects the source. Only use NEXT_PUBLIC_ for values that are safe to expose publicly.

Fix 2: Understand Where Variables Are Accessible

Next.js has two execution environments:

Locationprocess.env.SECRETprocess.env.NEXT_PUBLIC_VAR
pages/api/* (API routes)✅ Available✅ Available
getServerSideProps✅ Available✅ Available
getStaticProps✅ Available✅ Available
Server Components (App Router)✅ Available✅ Available
Client Components ("use client")❌ undefined✅ Available
Browser console / DevTools❌ undefined✅ Visible

App Router — server component (no prefix needed):

// app/dashboard/page.tsx — server component by default
export default async function DashboardPage() {
  const data = await fetch("https://api.example.com/data", {
    headers: {
      Authorization: `Bearer ${process.env.API_SECRET}`, // Works — server only
    },
  });
  // ...
}

App Router — client component (needs NEXT_PUBLIC_):

"use client";

export default function SearchBar() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL; // Works
  const secret = process.env.API_SECRET;           // undefined — never do this
  // ...
}

Fix 3: Fix .env File Loading Order

Next.js loads .env files in this order (later files override earlier ones for the same key):

  1. .env — base defaults, committed to git.
  2. .env.local — local overrides, not committed (add to .gitignore).
  3. .env.development / .env.production / .env.test — environment-specific.
  4. .env.development.local / .env.production.local / .env.test.local — local environment-specific overrides.

Common mistake — variable defined in wrong file:

# .env — committed, used as defaults
NEXT_PUBLIC_API_URL=https://api.example.com

# .env.local — local override (this wins)
# If this file exists but doesn't define NEXT_PUBLIC_API_URL,
# the .env value IS used. But if .env.local defines it as empty:
NEXT_PUBLIC_API_URL=
# Now the variable is an empty string, not the .env value

Check which file is winning:

# Print all env vars that start with NEXT_PUBLIC
node -e "require('dotenv').config({path:'.env.local'}); console.log(process.env.NEXT_PUBLIC_API_URL)"

Best practice file structure:

.env              # Safe defaults, committed (no secrets)
.env.local        # Local secrets, in .gitignore
.env.production   # Production defaults, committed (no secrets)

Fix 4: Fix Variables After Deployment (Vercel / CI)

On Vercel, Netlify, and other platforms, environment variables set in the dashboard are available at build time. NEXT_PUBLIC_* variables are baked into the bundle during next build — they are not dynamically injected at runtime.

If you add a variable after deploying, you must redeploy:

  1. Add the variable in your platform’s dashboard (Vercel → Settings → Environment Variables).
  2. Trigger a new deployment — the variable is included in the new build.
  3. The old deployment still has the old build (with undefined for the new variable).

Verify variables are set before build:

# Add to your build script to debug missing vars
echo "NEXT_PUBLIC_API_URL: $NEXT_PUBLIC_API_URL"
next build

For GitHub Actions:

- name: Build
  env:
    NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
  run: npm run build

Common Mistake: Setting environment variables in the hosting platform’s runtime environment config instead of the build environment config. For NEXT_PUBLIC_* variables, the build environment is what matters. Runtime-only env vars work for server-side code (API routes, server components) but not for client bundles.

Fix 5: Fix Runtime Environment Variables in App Router

Next.js 13+ App Router supports true runtime environment variables for server components — they are read at request time, not build time:

// app/api/data/route.ts — reads at runtime, not build time
export async function GET() {
  const dbUrl = process.env.DATABASE_URL; // Always current value
  // ...
}

For client-side runtime variables (not baked in at build time), Next.js does not support this natively for NEXT_PUBLIC_* vars. Options:

Option A: Expose vars via an API route:

// app/api/config/route.ts
export async function GET() {
  return Response.json({
    apiUrl: process.env.API_URL, // Server reads it
  });
}
// Client component fetches config at runtime
"use client";
const [config, setConfig] = useState(null);
useEffect(() => {
  fetch("/api/config").then(r => r.json()).then(setConfig);
}, []);

Option B: Use Next.js publicRuntimeConfig (Pages Router only):

// next.config.js
module.exports = {
  publicRuntimeConfig: {
    apiUrl: process.env.API_URL, // Set at server start, not build time
  },
};
import getConfig from "next/config";
const { publicRuntimeConfig } = getConfig();
console.log(publicRuntimeConfig.apiUrl);

Note: publicRuntimeConfig is not supported in App Router and disables static optimization.

Fix 6: Validate Required Environment Variables

Instead of debugging undefined errors at runtime, validate required variables at startup:

// lib/env.ts — validate at module load time
function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

export const env = {
  // Server-only
  databaseUrl: requireEnv("DATABASE_URL"),
  stripeSecretKey: requireEnv("STRIPE_SECRET_KEY"),

  // Client-safe (checked at build time)
  apiUrl: process.env.NEXT_PUBLIC_API_URL ?? "",
  stripePublicKey: process.env.NEXT_PUBLIC_STRIPE_KEY ?? "",
};

Using the t3-env library for type-safe env validation:

npm install @t3-oss/env-nextjs zod
// env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    STRIPE_SECRET_KEY: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },
});

This validates all env vars at startup with Zod schemas, throwing a descriptive error if any are missing or malformed.

Still Not Working?

Restart the development server. Next.js reads .env files at startup. After adding or changing .env.local, stop next dev and restart it — changes to .env files are not hot-reloaded.

Check for typos in variable names. process.env.NEXT_PUBLIC_API_URL and process.env.NEXT_PUBLIC_API_url are different. Environment variable names are case-sensitive.

Check for spaces around = in .env files. API_KEY = value (with spaces) is invalid in most .env parsers — use API_KEY=value (no spaces).

Check for quotes in .env files. Some .env parsers strip quotes, others do not. API_KEY="my value" may result in "my value" (with quotes) depending on the parser. Next.js strips surrounding quotes, but be careful with nested quotes.

Check next.config.js for env overrides. Values defined in next.config.js under env take precedence over .env files:

// next.config.js
module.exports = {
  env: {
    NEXT_PUBLIC_API_URL: "https://hardcoded.example.com", // Overrides .env.local
  },
};

For general .env loading issues outside of Next.js, see Fix: .env variables not loading.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles