Implementing a sane backend in Node.js using NestJS: Database connection and the ORM

Piotr Jaworski
Jit Team
Published in
5 min readAug 31, 2021

Welcome to the third part of a series of articles about writing a real-life scalable, performant, and maintainable backend in Node.js. In the previous parts, we’ve discussed the initial, high-level architecture of our new NestJS project; then, we set it up and defined the project's structure. This time it’s time to set up the database, the ORM, and the connection between our NestJS application and the database.

First, of course, we need a Docker client. After we download and install it, we need one command to get the containerized database up and running:

docker run -d --name postgres-dev -e POSTGRES_PASSWORD=pass1234 -v ${HOME}/postgres-data/:/var/lib/postgresql/data -p 5432:5432 postgres:alpine

Let’s see what’s happening here:

  • using docker run we, quite obviously, run the Docker container,
  • -d runs it in detached mode — which means that it won’t quit after we close the terminal we ran the command in; it also means that it needs to be stopped manually,
  • --name postgres-dev names our container for quick reference; this way, we’ll be able to docker stop postgres-dev and docker start postgres-dev,
  • -e POSTGRES_PASSWORD=pass1234 passes (pun intended) an environmental variable to the container — PostgreSQL requires a password (or a particular setting that allows omitting the password) to start the instance,
  • -v ${HOME}/postgres-data/:/var/lib/postgresql/data adds a volume that’s mapped to a directory in a subsystem (careful: it might be required to change this syntax a bit depending on the shell you’re using, e.g., in fish it would need to be $HOME/... instead of ${HOME}/...),
  • -p 5432:5432 maps the default PostgreSQL’s port to the Docker host,
  • postgres:alpine defines the image that the container should be created from.

Whew, that was quite a bit. Luckily, this command only needs to be run once. After that, we can use the docker start and docker stop commands to control the container. Of course, this setup is only valid for local development. For real deployments, we could, for instance (pun intended again), use a dedicated instance, like AWS RDS or Azure Database.

To do so, we’d need to pass different environment variables depending on the environment type. For that, we need to install an additional package:

npm i -DE cross-env

This package makes it possible to reliably (in terms of different development setups) pass env variables to npm scripts. We also need one more — dotenv — to differentiate env variables based on the instance, but that one is actually included in NestJS, we don’t need to install it manually.

Now, we can proceed to pass environment variables to our NestJS application. We just need to do one more thing to prevent the bad, bad people on the Internet from knowing all of our secrets. Since we’re going to put the credentials to the local database in the .env.local file, we need to add them to .gitignore to avoid pushing them to a remote repository. Of course, we all know that this never happens, but let’s just be sure, right? Just in case.

Why .env.local? That’s something nice that dotenv allows us — out of the box it looks for an .env file corresponding to the current NODE_ENV value, so if we make sure to run the local server with the right NODE_ENV setting, the values are going to be taken exactly from that file. On the other hand, in the actual environments, those are going to be injected from some form of an orchestration solution.

Once we got all of that going we can finally start connecting to the database! First, let’s install the ORM. As mentioned in Part I, it’s going to be TypeORM. Apart from that, we’re also adding a NestJS module for TypeORM and the PostgreSQL driver:

npm i -SE @nestjs/typeorm typeorm pg

Next, we can proceed with setting up the database connection. First, we need to prepare the environment files:

We’ve put all of the variable names into the .env file, but only the DB_PORT one has a value (it’s the default anyway), why do we keep the empty entries then? Well, done like this it’s pretty much self-documenting. On the other hand, all the “secret” variables are initialized in the gitignored .env.local file.

Let’s move on with the database connection! First, we need to be able to read the environment variables. We’ll create a configuration service that will make the variables available throughout the application. Let’s start with a custom configuration file, as per NestJS documentation:

What’s happening here? Two things, mainly. The last two properties tell TypeORM to automatically synchronize the database with the declared entities on the developer’s local machine and to automatically run migrations everywhere else. The rest of the properties allow TypeORM to connect to the actual database.

Once we have the custom configuration, we can start injecting it using the ConfigurationService. Let’s add some imports to the main module file:

There’s a lot happening there! We’ve basically added two imports. The first one is pretty simple — it’s the ConfigModule, which is using the already discussed custom configuration file and allows it to be injected using the ConfigService. The second one, on the other hand, is pretty huge — it’s the whole database configuration. Since we’ve added the ConfigModule in the previous import, we can now inject it to the TypeORM configuration. In order to do so, we need to use the forRootAsync TypeORM method instead of forRoot, import the ConfigModule inside the TypeORM config and inject the ConfigService inside the useFactory property. Then, we can retrieve all the properties using theConfigService‘s get method, and pass them on to TypeORM configuration along with some additional properties — most of them are for entity and migration configuration, but there’s also one to configure the naming strategy. It allows us to use camelCase in the code, which is then automatically mapped to snake_case in the database.

OK, is this the moment we’ve been waiting for? Can we take our app for a spin? Unfortunately, no — running now would result in an error; the local database instance doesn’t contain a database we specified in the configuration! We could write some scripts to create it, but there’s a simpler way — we’re going to use a typeorm-extension package. After adding it to the dependencies we can add a short snippet to our main.ts file:

As you can probably see, we’re using the custom configuration file that we defined for the TypeORM setup. This way, if we run this application for the first time, if the database is not present, it will get created right away! The synchronize setting also makes sure that the entities that we defined in the previous parts get an accurate representation in the database.

That wraps it up for today — you can check the code for the first three parts here. Also, stay tuned for the next part. Sign up for our newsletter to stay up to date, and if you’re interested in how we can help you use technology to empower your business, be sure to visit our website!

This article is the first of the series about building a well-structured API in Node.js using NestJS and PostgreSQL:

--

--