- Published on
Validating environment variables in Nest.js
- Authors
Introduction
It's paramount to ensure the reliability and security of our code. One often overlooked aspect is the proper validation of environment variables. These seemingly little pieces of information play a crucial role in configuring and adapting your applications to various deployment environments.
Environment variables serve as a bridge between your code and the underlying infrastructure, influencing how your application behaves across different environments, such as development, testing, staging, and production. Neglecting to validate these variables can lead to a host of issues, ranging from simple configuration errors to critical security vulnerabilities, ultimately jeopardizing the functionality and integrity of your applications.
For example, you need to connect your application to different databases, for development to your local database, and production to a cloud database with all the production data. You can steer this connection based on individual environment variables like the database host, port, user, password, and much more. As you can guess, this is a critical part of the application where a missing environment variable or a wrong type can cause havoc because your application can't connect correctly to the database.
How cool would it be to validate the environment variables first and then log the result of the failed validation, so that you can see what was wrong immediately without hours of debugging while your application is useless?
That's why I've written this blog post, where we will delve into the reasons why validating environment variables is not just a best practice but a necessity in the realm of software development. Whether you're a seasoned developer or a newcomer to the field, understanding the significance of proper environment variable validation will empower you to build more robust, secure, and resilient applications.
To take this step further, we will see how we can do this in Nest.js and take a look at two common ways to accomplish that.
Validation techniques
A very common use case for environment variables is connecting an API to a database. So, we're going to spin up a Nest.js application and a MySQL database and connect it via environment variables.
Let's start with generating a new Nest.js application:
nest new env-validation
Setup
Now, create a new Dockerfile
in the root of the project and add this code:
FROM node:alpine as development
WORKDIR /usr/src/app
COPY package.json ./
COPY package-lock.json ./
COPY tsconfig.json tsconfig.json
COPY nest-cli.json nest-cli.json
RUN npm install -g pnpm
COPY ./ ./
RUN pnpm install -r
RUN pnpm run build
FROM node:alpine as production
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package.json ./
COPY package-lock.json ./
RUN npm install -g pnpm
RUN pnpm install --prod
COPY /usr/src/app/dist ./dist
CMD ["node", "dist/main.js"]
If you have got some experience with Docker, you see that this is nothing fancy, but just a pretty straightforward multi-stage Docker file.
We can orchestrate our containers now inside a new docker-compose.yaml
in the project root:
services:
mysql:
image: mysql
env_file:
- .env
ports:
- '3306:3306'
api:
build:
context: .
dockerfile: ./Dockerfile
target: development
command: npm run start:dev
ports:
- '3000:3000'
env_file:
- ./.env
volumes:
- .:/usr/src/app
depends_on:
- mysql
Great! I have decided to use typeorm
as our ORM in this tutorial. To use our environment variables in the Nest.js way, we need to add the dependency for our ConfigService
. Let's install a few dependencies :
npm i @nestjs/typeorm typeorm mysql2 @nestjs/config
We can configure the TypeOrmModule
in our app.module.ts
:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => {
return {
type: 'mysql',
host: configService.get('MYSQL_HOST'),
port: configService.get('MYSQL_PORT'),
database: configService.get('MYSQL_DATABASE'),
username: configService.get('MYSQL_USERNAME'),
password: configService.get('MYSQL_PASSWORD'),
synchronize: configService.get('MYSQL_SYNCHRONIZE'),
autoLoadEntities: true,
};
},
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
As you can see, we have set up our ConfigModule
and the TypeOrmModule
, where we inject the ConfigService
and define the different properties that we need for our database connection. The ConfigService
helps us to read environment variables and use them inside our application.
However, when we now run:
docker compose up
we can see that we can't connect to our database, because our .env
file is empty.

The error message itself is pretty generic. Unable to connect to database doesn't help us pretty much, what exactly went wrong. Of course, we know it happens because our .env
file is still empty and we haven't provided database credentials at all. But imagine a scenario where we have many different environment variables or simply don't know what exact variables are needed for this, because normally a .env
doesn't get committed to the repository. Of course, you can solve this by adding an env-template.md
file for example, but that's not common in every project I have seen.
That's where environment variable validation comes into play! It checks the required environment variables before trying to connect to the database, which is a cleaner way to handle those use cases.
joi
Way 1: Schema Validation with Let's start with making a schema validation using the npm package joi
. First, we install the package with npm i joi
.
Inside our app.module.ts
we're importing it like this:
import * as Joi from 'joi';
On the ConfigModule.forRoot
method is a property called validationSchema
, which we're leveraging to perform our validation. Add the following code to your existing ConfigModule.forRoot
:
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
MYSQL_HOST: Joi.string().required(),
MYSQL_PORT: Joi.number().required(),
MYSQL_DATABASE: Joi.string().required(),
MYSQL_USERNAME: Joi.string().required(),
MYSQL_PASSWORD: Joi.string().required(),
MYSQL_SYNCHRONIZE: Joi.boolean().required(),
}),
}),
// ... other code
As you can see, we have created a new key for every environment variable we need to start up our application properly. After that, we utilized joi
to tell the ConfigModule
that our environment variables are required and what type we're expecting right here.
When you now start up the development server with:
docker compose up
and still have an empty .env
file, you see a more detailed output than before:

Now we're validating the environment variables first before trying to connect to our database. Let's fill our .env
file with the correct values:
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_DATABASE=database
MYSQL_USERNAME=root
MYSQL_ROOT_PASSWORD=root
MYSQL_PASSWORD=root
MYSQL_SYNCHRONIZE=true
The MYSQL_ROOT_PASSWORD
is a required environment variable that our mysql
image needs. For more information check https://hub.docker.com/_/mysql.
When we now start everything up again with:
docker compose up
our application should start properly without any errors:

Play around a little bit. Insert a wrong value for a variable, provide a wrong type, or misspell a variable name. It's important to get your hands a bit dirty with it on your own to understand what this technique can do and what not.
getOrThrow
method
Way 2: Using the Besides schema validation, there is also another very common technique. It's using the getOrThrow
method on the ConfigService
directly without running our environment variables through a validation schema first.
Let's jump into our main.ts
file and get our server port also as an environment variable and use getOrThrow
.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 👇 Add this code
const configService = app.get(ConfigService);
const port = configService.getOrThrow<number>('PORT');
await app.listen(port);
}
bootstrap();
When we start the containers again, we can see:

This was something we expected, right? Using the getOrThrow
method throws an error when it can't find the provided environment variable. In our case, we haven't provided a PORT
environment variable. So the behavior is exactly what we want.
Let's provide a new variable inside our .env
:
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_DATABASE=database
MYSQL_USERNAME=root
MYSQL_ROOT_PASSWORD=root
MYSQL_PASSWORD=root
MYSQL_SYNCHRONIZE=true
PORT=3000
Starting all up again, we don't see any errors and our application starts up properly:

Conclusion
As you can see, validating environment variables in Nest.js is pretty straightforward and important to implement. You have seen two different techniques, that are pretty common and used very often.
The implementation differs a bit, but the result is pretty much the same. You receive a proper validation of the required environment variables before they're used inside your application. However, there is a slight difference between the schema validation and the getOrThrow
approach.
With the help of schema validation, you can also specify what data type you're expecting, while using getOrThrow
just checking if the environment variable is provided or not. This is a bit more powerful in my opinion. You are also more flexible in configuring required and optional environment variables. Using joi
you can also perform a more detailed validation. For example, you can specify how many characters a string at least should have or between which numbers a port variable should be. Check out the docs to get more information about that!
But at the end of the day, it comes to project specifications and a personal taste of what exact way to use here. There are of course some other ways out there. But having no proper validation of your environment variables is no option at all!
I hope you had as much fun as me writing this blog post right here! I wish you much fun with the other posts right here and love to receive any feedback.
Cheers,
Jean-Marc