Type-Safe Environment Variables in Next.js (And Why You Need Them)
Next.jsTypeScriptBackend

Type-Safe Environment Variables in Next.js (And Why You Need Them)

A small chunk of TypeScript that prevents an entire class of production bugs. Validating environment variables at build time with Zod and t3-env.

HJ
Hassan Javed
March 2026
7 min read

The bug that kills a Friday afternoon

You deploy a feature to production. Five minutes later, customers report errors. The app crashes on first request with a TypeError saying cannot read property of undefined. You dig in. The cause: a new environment variable you added locally but forgot to set on Vercel.

This bug is preventable. The fix takes 20 minutes once, then never bites you again. This post is the fix.

The pattern

The idea: validate all environment variables at build time, with TypeScript types that flow through your entire app. If a required variable is missing or has the wrong shape, the build fails loudly instead of silently in production.

Tools:

Zod for the validation schema
t3-env (a tiny library from the T3 stack folks) for the wiring
TypeScript for the inferred types

Total install: 2 packages. Total code: about 40 lines.

Setup

In your Next.js project root, install the two packages: @t3-oss/env-nextjs and zod.

Create an env.ts file at the root. Inside, you call createEnv with three sections:

1.server — variables only available on the server (secrets, DB URLs)
2.client — variables exposed to the browser (must start with NEXT_PUBLIC_)
3.runtimeEnv — explicit mapping of names to process.env values (Next.js needs this so it can inline at build time)

For each variable you list the Zod validator. For example, DATABASE_URL gets z.string().url(), STRIPE_SECRET_KEY gets z.string().min(1), NODE_ENV gets z.enum with development, test, production options.

How you use it

Anywhere in your app, import env from your env.ts file and access variables with full type safety. On the server, you can use env.STRIPE_SECRET_KEY when creating your Stripe client. On the client, you can use env.NEXT_PUBLIC_APP_URL freely.

If you try to access a server-only variable in client code, TypeScript errors at compile time. If you try to use a variable name that does not exist in the schema, TypeScript errors. If you misspell a variable, TypeScript errors.

What happens when a variable is missing

When you run npm run build, if STRIPE_SECRET_KEY is not set, the build fails immediately with a clear message saying invalid environment variables, pointing at the specific variable, with the exact validation error.

Clear error. Specific variable. Exact reason. No mystery bug in production at 3pm on Friday.

Validating beyond "not empty"

Zod gives you the full validation language. Use it.

DATABASE_URL — z.string().url()
PORT — z.coerce.number().int().positive()
LOG_LEVEL — z.enum with debug, info, warn, error, defaulting to info
FEATURE_FLAG — z.coerce.boolean() with a default
ALLOWED_ORIGINS — z.string().transform that splits on comma into an array

Now PORT is a number (not a string). ALLOWED_ORIGINS is an array. LOG_LEVEL is one of four literals. All checked at build time.

The gotchas

Vercel and other platforms

The validation only catches missing variables if you run a build that has access to all environments. On Vercel, this just works — Vercel injects your env vars during the build. On other platforms, make sure your CI sets all env vars before building.

Skip validation locally (sometimes)

For quick local experiments where you do not want to set every variable, t3-env lets you skip validation with SKIP_ENV_VALIDATION=true. Use sparingly.

Optional variables

Mark optional variables explicitly with .optional() at the end of the Zod chain. If they are missing, the value is undefined and the build succeeds. If they are set but malformed, the build fails.

Default values

For non-secret variables with sensible defaults, chain .default("value") on the Zod validator. Saves you from having to set every variable in every environment.

Why this matters

The before-and-after:

Before: A missing env var is a runtime undefined error that surfaces minutes or hours after deploy, often in a flow most developers do not test locally.

After: A missing env var fails the build with a specific error pointing at the variable, the schema, and what was wrong.

The first scenario costs a Friday afternoon. The second costs a fix in 30 seconds.

For any production app, this is one of the highest-leverage changes you can make. 20 minutes of setup, eliminates an entire class of bugs forever.

My env setup in 2026

t3-env for the wiring
Zod for validation
Strict mode — no implicit optional
Group related variables (auth, payments, observability) with comments
A single env.ts at the project root, imported everywhere
A .env.example file committed to the repo as documentation

TL;DR

Set up t3-env plus Zod for type-safe environment variables
Validates at build time, fails loudly when something is missing
40 lines of code, eliminates a class of bugs forever
Use Zod's full validation language for complex variables (numbers, enums, arrays)
Commit a .env.example file as documentation

If you are setting up a new Next.js project or want a senior to audit your existing setup, contact me.

Related Reads

You might also like