CalcSnippets Search
Web 3 min read

Next.js Environment Variables With App Router: What Actually Runs on the Server vs the Browser

A practical App Router guide to `.env` files, `NEXT_PUBLIC_`, server-only values, load order, and the mistakes that leak secrets or make developers think env vars are “not working.”

Why this topic keeps biting teams: Next.js environment variables are not complicated once you separate server-side execution from browser-side execution. Most confusion comes from forgetting where the code runs.

The core rule you must remember

In Next.js, not every environment variable belongs in the browser.

If a variable needs to be available in client-side JavaScript, it must be prefixed with:

NEXT_PUBLIC_

If it does not have that prefix, treat it as server-only.

That one rule explains a huge number of “why is process.env.MY_VAR undefined?” bugs.

A clean .env.local example

DATABASE_URL=postgresql://app:secret@localhost:5432/app
OPENAI_API_KEY=sk-xxxxx
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001

Meaning:

  • DATABASE_URL is server-only
  • OPENAI_API_KEY is server-only
  • NEXT_PUBLIC_API_BASE_URL can be bundled for browser code

Server component example

This is valid in a server component or route handler:

export default async function Page() {
  const dbUrl = process.env.DATABASE_URL;

  return <div>Server is configured: {dbUrl ? "yes" : "no"}</div>;
}

That works because the code executes on the server.

Client component example

This will not work as expected:

"use client";

export default function ClientWidget() {
  return <div>{process.env.OPENAI_API_KEY}</div>;
}

That should stay undefined, and that is a good thing. A secret API key does not belong in browser code.

What should work:

"use client";

export default function ClientWidget() {
  return <div>{process.env.NEXT_PUBLIC_API_BASE_URL}</div>;
}

Why people think env vars are broken

Usually one of these reasons:

  1. they forgot NEXT_PUBLIC_ for browser use
  2. they changed .env.local without restarting the dev server
  3. they are reading the variable from client code when it is server-only
  4. they committed code assuming all process.env.* values behave the same

Restart matters more than people like

If you change an env file, restart the dev server:

npm run dev

or:

pnpm dev

depending on the project.

Do not waste 20 minutes debugging stale process state.

A safe pattern for client apps

If your frontend needs to call a backend, expose the backend base URL publicly:

NEXT_PUBLIC_API_BASE_URL=https://api.example.com

Then use it in client code:

"use client";

const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL;

export async function loadProfile() {
  const response = await fetch(`${apiBase}/profile`);
  return response.json();
}

Keep secrets on the server. Let the browser know only what it must know.

Server-only route example

If you need to call a secret-backed API, do it in a route handler or server action:

export async function GET() {
  const apiKey = process.env.OPENAI_API_KEY;

  return Response.json({
    configured: Boolean(apiKey),
  });
}

That protects the secret from browser exposure.

The deeper App Router lesson

App Router makes server and client code live closer together, which is powerful but also the source of confusion. The fix is to stop asking “does Next.js support env vars?” and start asking “where is this code executing?”

If the answer is “browser,” only NEXT_PUBLIC_ values belong there. If the answer is “server,” you can use private variables normally.

That separation is not a nuisance. It is one of the ways Next.js helps prevent people from leaking secrets accidentally.

The practical shortcut is this: if the file is a client component, or it touches browser-only APIs, stop expecting normal server secrets to be there. That one habit prevents a surprising amount of wasted debugging time.

Sources

Keep reading

Related guides