5 min read
In Node.js applications, we often use environment variables for configuration. We can access them via process.env
, but we have to be careful. We don't know if the variable we are looking for is actually there, and process.env
always returns a string. Therefore, we often need to convert the value to the type we need.
If something is wrong with the environment (e.g., a variable is missing or has the wrong type), we often don't notice it until we use the variable at runtime, and the error message is often not very helpful.
In this post, we will try to solve this problem using TypeScript and Zod.
Sample application
Let's say we have a simple application that loads its environment using the dotenv package. The application requires the following environment variables:
Variable | Type | Default value |
---|---|---|
HOST | string | localhost |
PORT | number | 3000 |
string | - | |
URL | string | - |
NODE_ENV | enum | development |
To specify the environment variables, we create an .env
file in the root directory of our project.
Now, we can load the environment variables using dotenv.
After loading the environment variables, we can access them through process.env
.
But here, our problems begin. We are unsure if the variable exists, and we receive it as a string. Therefore, the following code will not successfully pass the TypeScript compiler.
To fix this, we need to verify the existence of the variable and convert it into a number.
We have to do this for every variable we need. This is not only annoying, but also error-prone. If we forget to check a variable, we will only notice it when we run the application.
Type-safe environment
To solve this problem, we will create a type-safe version of process.env
.
First, we create a new file called env.ts
, which loads, validates and converts our environment variables.
The code above loads the environment, as we have seen before. After that, it creates a schema with Zod. The schema defines the structure of our environment variables. We will look at the schema in a moment, but for now, we define it as an empty object. Once the schema is defined, we parse the environment variables using the schema. If the parsing fails, we log an error and exit the process. Finally, we export the parsed environment variables.
The beauty of this approach is that we only have to do this once, and we can import it from wherever we want. The best part is that it is completely type-safe.
We can access the environment variables as follows:
We no longer need to check if the variable exists or convert it. Zod handles it for us, and TypeScript no longer yells at us.
If an environment variable is missing or has the wrong format, we will encounter an error upon application startup. Now let's take a look at the schema.
- HOST: The host is a string and cannot be empty. If the variable is missing, we use the default value
localhost
. - PORT: The port must be converted to a number. We use the
coerce
method for this. The port must be an integer and positive. If the variable is missing, we use the default value3000
. - EMAIL: The email is a string and cannot be empty. It must be a valid email address.
- URL: The URL is a string and cannot be empty. It must be a valid URL.
- NODE_ENV: The node environment is an enum and must be one of the following values:
development
,production
ortest
. If the variable is missing, we use the default valuedevelopment
.
As we can see, Zod makes it really simple to define a complex schema for our environment variables, and this is just the tip of the iceberg for what can be done with Zod. If you want to learn more about Zod, check out the documentation or the wonderful course by Matt Pocock.
Conclusion
In this post, we have seen how to create a type-safe version of process.env
with TypeScript and Zod. This approach is not only type-safe but also makes working with environment variables easier.
The approach we have used in this article works fine for most applications, but there are edge cases where it does not work as well, especially in frameworks where the environment is used on both the server and the client. In these cases, it is better to use a library designed specifically for this purpose, such as T3 Env.
Posted in: typescript, zod, environment