Skip to main content

How to Validate Environment Variables When a Next.js App Starts

· 7 min read
Jagdish Kumawat
Founder @ Dewiride

A missing or mistyped setting is one of the most common reasons a deployed app fails — usually on the first request that needs it, with an error far from the cause. This guide checks every setting of a Next.js app once, when the server starts: a zod schema says what each setting must look like, and instrumentation.ts runs the check. The result is one clear message that names each wrong setting.

Where the Check Runs​

Next.js runs one file when its server starts: instrumentation.ts, in the root of the project. Its documentation says the file exports a register function that is called once when a new Next.js server instance is initiated, and must complete before the server is ready to handle requests. An error thrown there fails the server's start, which is exactly what a wrong setting should do.

zod describes what valid data looks like and checks values against that description, collecting every problem with a readable message.

Prerequisites​

  • A Next.js app with the App Router. The examples use Next.js 16.4.6.
  • A terminal in the app's folder.

Step 1: Install zod​

Terminal
npm install zod@4.6.7
Output

added 1 package, and audited 33 packages in 1s

8 packages are looking for funding
run `npm fund` for details

found 0 vulnerabilities

Step 2: Describe the Settings​

This example app needs two settings: the address of an API, and the name it shows. Create lib/env.ts, starting with the description of each setting:

lib/env.ts
import "server-only";
import { z } from "zod";

// The settings this app needs, and what a valid value looks like.
const EnvSchema = z.object({
API_URL: z.url({
protocol: /^https?$/,
error: "API_URL must be a full http:// or https:// address.",
}),
SITE_NAME: z
.string({ error: "SITE_NAME is required." })
.trim()
.min(1, "SITE_NAME must not be empty."),
});
CodeWhat it checks
import "server-only"The file is only ever used on the server (How to Keep Server-Only Code Out of the Browser in Next.js)
z.url({ protocol: /^https?$/ })A complete address that starts with http or https. localhost:5000 fails
error: "…"The message shown when the check fails — written for the person who sets the value
z.string(…).trim().min(1, …)Text that isn't empty or only spaces

Then a function that checks process.env against the schema:

lib/env.ts
export type Env = z.infer<typeof EnvSchema>;

export function getEnv(): Env {
const result = EnvSchema.safeParse(process.env);
if (!result.success) {
throw new Error(
`The app's settings are missing or invalid.\n${z.prettifyError(result.error)}`,
);
}
return result.data;
}

safeParse checks every setting and collects all the problems instead of stopping at the first one. z.prettifyError turns them into one readable list. z.infer makes a TypeScript type from the schema, so the rest of the app gets API_URL and SITE_NAME with their types.

macOS-style code window titled lib/env.ts showing a zod schema for API_URL as an http or https URL and SITE_NAME as non-empty text, and a getEnv function that safeParses process.env and throws with z.prettifyError

Step 3: Run the Check When the Server Starts​

Create instrumentation.ts in the root of the project, next to package.json:

instrumentation.ts
// Next.js calls register() once, when the server starts.
// Checking the settings here stops a misconfigured app before it serves a page.
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { getEnv } = await import("./lib/env");
getEnv();
}
}
CodeWhat it does
process.env.NEXT_RUNTIME === "nodejs"Next.js can run server code in two runtimes, Node.js and Edge. The check runs in Node.js
await import("./lib/env")Loads the settings module only inside that check
getEnv()Throws when a setting is wrong. The result isn't needed here

Step 4: See the Check Work​

With no settings at all, start the app:

Terminal
npm run dev
Output
npm notice run my-app@0.1.0 dev
npm notice run next dev
▲ Next.js 16.4.6 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.10:3000
✓ Ready in 268ms
✓ Running next.config.ts took 2.0s
Error: An error occurred while loading instrumentation hook: The app's settings are missing or invalid.
✖ API_URL must be a full http:// or https:// address.
→ at API_URL
✖ SITE_NAME is required.
→ at SITE_NAME
at getEnv (lib/env.ts:21:11)
at Module.register (instrumentation.ts:6:5)

Both problems in one message, each with the setting's name — and npm run dev stopped. Note the order: ✓ Ready appears first, and the error from register follows it.

Now a .env.local with a typical mistake, an address without http://:

.env.local
API_URL=localhost:5000
SITE_NAME="My App"
Output (npm run dev)
Error: An error occurred while loading instrumentation hook: The app's settings are missing or invalid.
✖ API_URL must be a full http:// or https:// address.
→ at API_URL

Only the wrong setting is reported. With API_URL=http://localhost:5000, the app starts normally:

Output (npm run dev)
▲ Next.js 16.4.6 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.10:3000
- Environments: .env.local
✓ Ready in 213ms
✓ Running next.config.ts took 23ms
- Cache Components enabled
- Partial Prefetching enabled

How It Behaves in Production​

The check runs when a server starts, so it behaves differently in the three commands:

CommandWithout the settings
npm run buildBuilds normally: the check doesn't run during the build. The settings belong to the place where the app runs, not to the build server
npm startLogs Failed to prepare server Error: An error occurred while loading instrumentation hook with the same list and keeps running — but every request I sent got 500. Restart it with the right settings
npm run devStops, as above

So in production a wrong setting never produces a half-working page: every request fails, and the log names the setting. Watch your hosting platform's logs after a deployment.

Common Mistakes​

MistakeWhat happensDo this instead
Reading process.env directly all over the appA missing setting fails on the first request that needs it, far from the causeRead settings through getEnv()
parse instead of safeParsezod throws its own error format; you lose the friendly listsafeParse and z.prettifyError
Leaving out the NEXT_RUNTIME checkThe settings module may be loaded in the Edge runtime tooCheck process.env.NEXT_RUNTIME === "nodejs"
Naming settings NEXT_PUBLIC_…Next.js copies them into the browser's JavaScript at build timeKeep server settings without the prefix

Troubleshooting​

SymptomLikely causeFix
An error occurred while loading instrumentation hookregister threw: a setting is wrongRead the ✖ lines and fix those settings
The check never seems to runinstrumentation.ts is inside app/ or lib/Put it in the project root, or in src/ if you use one
Every page answers 500 after deployingThe production server's settings are wrongCheck the log for the instrumentation error

FAQ​

Why not check the settings in next.config.ts?​

next.config.ts runs during the build too, so a check there would fail builds on machines that don't have the runtime settings — such as a build server.

Can other parts of the app use getEnv()?​

Yes. Call it wherever you need a setting: it returns typed values, and it throws the same clear message if something changed.

Conclusion​

A zod schema in lib/env.ts and a three-line register function in instrumentation.ts check every setting the moment the server starts. npm run dev stops with one message per wrong setting, a production server answers 500 and logs the same message, and the build itself never needs the settings.

Additional Resources​