Type-Safe Env Vars in Next.js
The Problem
When working with Next.js, you can set up environment variables using .env
files and access them through process.env
.
# .env
DB_HOST=localhost
DB_USER=myuser
DB_PASS=mypassword
// src/db.ts
connectDB({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password, process.env.DB_PASS,
});
While this is convenient, it comes with a TypeScript limitation: any environment variable you access will be typed as string | undefined
. This means you won't get proper autocompletion in your IDE and making typos won’t generate any type error.
Making Env Vars Type-Safe
Matt Pocock suggested a smart solution using Zod to add type safety to your process.env
:
// src/env.ts
import { z } from "zod";
const env = z.object({
DB_HOST: z.string(),
DB_USER: z.string(),
DB_PASS: z.string(),
});
const result = env.safeParse(process.env);
if (!result.success) {
console.error(
'Error happened when parsing process.env:',
JSON.stringify(result.error.errors, null, 2),
);
throw new Error('process.env parsing error');
}
declare global {
namespace NodeJS {
interface ProcessEnv extends z.infer<typeof env> {}
}
}
Once implemented, your IDE will provide proper autocompletion and process.env.DB_HOST
will be correctly typed as a string
.
Validating process.env
at Build Time
Then we need to execute validation logic (env.parse(process.env)
) to actually parse process.env
. Doing it at build time is a great option.
The T3 Env package offers an excellent approach using the jiti library to validate the env vars in the next.config.js
file:
// next.config.js
import { fileURLToPath } from "node:url";
import createJiti from "jiti";
const jiti = createJiti(fileURLToPath(import.meta.url));
jiti("./src/env");
/** @type {import('next').NextConfig} */
export default {
/* ... */
};
This means if there's any problem with your environment variables, the build will fail.
If you are using TypeScript for the Next config (next.config.ts
), you can directly import the src/env.ts
file in the config file:
// next.config.ts
import type { NextConfig } from 'next';
import "./src/env";
const nextConfig: NextConfig = {
/* ... */
};
export default nextConfig;
That's it! With this setup, your process.env
is now fully type-safe.
For even more functionality, check out the T3 Env package. But note that it takes a different approach where you import environment variables from a module rather than using process.env
directly.