Implementing a sane backend in Node.js using NestJS: Database connection and the ORM
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 todocker stop postgres-dev
anddocker 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., infish
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: