Lesson 27 of 81
One typo in a variable name is invisible until it isn't — and the two most common ways to get environment variables wrong in Next.js are opposite mistakes: leaking a secret to the browser, and expecting a server-only value to update after the build already froze it.
10 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A team builds a Docker image with `next build` and promotes the same image from staging to production. `NEXT_PUBLIC_API_URL` differs between the two environments. What happens?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Next.js loads environment variables from .env* files into process.env automatically, no extra package required. But which file wins when several define the same variable, and which values a browser can actually see, both follow specific rules.
Load order (Next.js stops at the first place it finds a variable): process.env (already-set shell/platform variables) → .env.$(NODE_ENV).local → .env.local (skipped entirely when NODE_ENV is test) → .env.$(NODE_ENV) → .env. A value in .env.production.local beats the same variable defined in .env.
Visibility is the other axis, and it's independent of load order. By default every variable is server-only — it lives in process.env on the server and never reaches the browser. Prefix a variable with NEXT_PUBLIC_ and Next.js does something different: at next build time, it finds every reference to process.env.NEXT_PUBLIC_X in your code and replaces it with the literal value, baking it into the JavaScript bundle shipped to the browser. That's not "exposed if someone digs" — it's inlined, plain-text, in a file anyone can view-source.
// .env.production
DATABASE_URL=postgres://prod-host/db
NEXT_PUBLIC_ANALYTICS_ID=abc123
// app/api/orders/route.ts — server-only, never bundled
export async function GET() {
const db = await connect(process.env.DATABASE_URL) // fine, server-only
return Response.json(await db.orders.findMany())
}
// app/analytics.tsx — inlined into the client bundle at build time
'use client'
setupAnalytics(process.env.NEXT_PUBLIC_ANALYTICS_ID) // literally becomes "abc123"
DATABASE_URL never leaves the server; NEXT_PUBLIC_ANALYTICS_ID becomes a hardcoded string the moment next build runs. This is why the rule is never "add NEXT_PUBLIC_ and restrict it later" — once it's in a shipped bundle, restricting the source variable doesn't unship the value.
next.config.ts is where you configure the build and runtime behavior itself — not application secrets. This repo's own next.config.ts at the project root is currently just {} (an empty NextConfig object), which is normal: Next.js is zero-config by default, and you only add keys like output, images, or turbopack when you need to override a specific default.
NEXT_PUBLIC_ variables are frozen at build time, not read at runtime. Promoting the same build artifact to a new environment doesn't pick up new NEXT_PUBLIC_ values — they were already inlined into the JS the last time next build ran..env.local is silently skipped in the test environment, specifically so test runs produce the same result for every developer regardless of local overrides.process.env[varName] are never inlined, even for NEXT_PUBLIC_ variables — Next.js only replaces literal process.env.NEXT_PUBLIC_X references it can statically find.process.env itself (already-set shell variables) outranks every .env* file — it's checked first in the load order, which is exactly what lets a CI platform override a committed .env value without touching the repo./src directory does not move where .env* files live — they stay at the project root even when your app code lives under src/."How would you make sure an API key never ends up in the browser bundle, and how would you promote one build across staging and prod without rebuilding?" tests whether a candidate actually understands the build-time-inlining vs. runtime-read distinction, not just that NEXT_PUBLIC_ "makes something public."