process.env validation with Zod

Wojciech Trawiński
JavaScript everyday
3 min readJul 18, 2024
Photo by Jeremiah Ross on Unsplash

As a web developer, I have always seen Zod as a popular library widely used for parsing server responses at runtime.

However, Zod’s utility does not end there. It is also incredibly versatile and can be used to parse input data, ensuring accuracy and integrity. I will cover this in the blog post.

Let’s assume you have a script that runs as part of a continuous integration process. The input data is passed using environment variables:

function deployApp() {
const { APP_NAME, DEPLOYMENT_ENV } = process.env;

//...
}

deployApp();

The values can be defined in a configuration file or via a GUI. For example, when you run a pipeline manually in GitLab CI/CD or start a manual workflow using GitHub Actions.

The script implementation is beyond the scope of this blog post, but the key point is that in the first step, it needs to examine environment variables. A simple implementation may look as follows:

function deployApp() {
const { APP_NAME, DEPLOYMENT_ENV } = process.env;

const isValidAppName = APP_NAME !== undefined;
const isValidDeploymentEnv = DEPLOYMENT_ENV !== undefined && ['dev', 'staging', 'prod'].includes(DEPLOYMENT_ENV);

if (!isValidAppName || !isValidDeploymentEnv) {
throw new Error('Invalid environment variables');
}

const deployOptions = { appName: APP_NAME, env: DEPLOYMENT_ENV } as DeployOptions;

//...
}

After successful validation, the script may continue to run. However, you need to add type annotations for the parsed input data:

interface DeployOptions {
appName: string;
env: 'dev' | 'staging' | 'prod';
}

The thing is, you need to implement validation logic from scratch and keep it in sync with type annotations. Although this may not be an issue for simple cases, it can easily become hard to maintain.

Turns out, you can easily have a single source of truth using the Zod library. First, define a schema that covers necessary validation and parsing:

import { z } from 'zod';

const DeployOptionsSchema = z
.object({
APP_NAME: z.string(),
DEPLOYMENT_ENV: z.enum(['dev', 'staging', 'prod']),
})
.transform(({ APP_NAME: appName, DEPLOYMENT_ENV: env }) => ({ appName, env }));

Next, use the schema in your script to parse the input data:

function deployApp() {
const deployOptions = DeployOptionsSchema.parse(process.env);

//...
}

The inferred type of the deployOptions variable is the same as the type explicitly defined based on the DeployOptionsSchema:

type DeployOptions = z.infer<typeof DeployOptionsSchema>;

There’s no need to define the type separately, and remember to keep it synced. This is a really nifty solution if you need complex validation rules and work with many environment variables.

The Zod library ensures the accuracy and integrity of input data, eliminating the need to manually maintain validation logic and type annotations. By defining a schema with Zod, developers can easily parse and validate environment variables, simplifying the process and reducing the potential for errors.

I hope you liked my blog post, thanks for reading! 🙂

--

--