Hello Backend devs...

Time to connect NestJS application with a database. There are many databases can be connected with NestJS, including Sql or NoSql types. Choosing a right database is another crucial decision in the application's architecture. Today, let's focus on connect with the PostgreSQL database - a powerful popular relational database.

Obviously, we can use native drivers to connect with any databases from NestJS app. But, most of time, it is better to use an ORM, since, it is more time efficient and maintainable.

Today, let's focus on Prisma ORM, one of the most modern and developer-friendly ORMs, to connect PostgreSQL with a NestJS application.

📌 Install dependencies

# adding prisma cli
$ pnpm add -D prisma

# prisma client for development
$ pnpm add @prisma/client

📌 Initialise Prisma

This will create prisma folder and .env file in the root

$ pnpx prisma init

Prisma Folder Structure

📌 Configurations

Need to update .env file for the database connection.

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

📌 Migrations

Schema file located at prisma/schema.prisma file. There are two ways to run migrations here,

  1. Migrations
    Mostly use this approach. We create a schema on code, then push the changes to the database.

  2. Introspection
    In here, When already have the database, we can update the schema by pulling it. Command in bellow,

$ pnpx prisma db pull

Let's go through the most common approach Migrations. In the first stage, we don't have anything in the database. We need to create it from the sketch. Assume that we need to create a user table. What we have to do is update the schema.prisma file as needed,

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum UserType {
  ADMIN
  USER
}

model Role {
  id         Int      @id @default(autoincrement())
  name       String   @unique
  created_at DateTime @default(now())
  User       User[]
}

model User {
  id         Int      @id @default(autoincrement())
  name       String
  email      String   @unique
  password   String
  role_id    Int
  type       UserType // custom type with enum
  created_at DateTime @default(now())
  Role       Role     @relation(fields: [role_id], references: [id])
}

Now, need to create the first migration file,

# pnpx prisma migrate dev --name 
$ pnpx prisma migrate dev --name init

This will do two actions.

  1. Create new migration file inside prisma/migrations folder
    Migration folder

  2. Create tables in the connected database provided in the schema file

Anyway, we have a option to only create the migration file without execute it,

$ pnpx prisma migrate dev --name  --create-only

We can execute this any time by,

$ pnpx prisma migrate dev

📌 Update table

What if we want to modify existing table or add a new table to the database?

Modify the prisma.schema file for the new change.

Best practices:

  • Single logical change/s per migration
  • Always, create a migration file for change using prisma migrate dev. This will re generate the prisma client and create a migration file.
  • Review migration without execute it using npx prisma migrate dev --create-only
  • Descriptive naming on migration file
  • Never edit executed migration files. Create a new one for change

Let's assume that we want to add optional age filed to the user table. Modify the user model.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Role {
  id         Int      @id @default(autoincrement())
  name       String   @unique
  created_at DateTime @default(now())
  User       User[]
}

model User {
  id         Int      @id @default(autoincrement())
  name       String
  email      String   @unique
  password   String
  age        Int? // ? mark indicate that optional
  role_id    Int
  created_at DateTime @default(now())
  Role       Role     @relation(fields: [role_id], references: [id])
}

Generate migration,

$ pnpx prisma migrate dev --name add-age-field-to-user --create-only
$ pnpx prisma migrate dev

📌 Deploy in production

When we want to apply migrations on production by manually or using CI/CD, then much safe to use,

$ pnpx prisma migrate deploy

Why it safe for production? This does not generate any migrations. Only apply the changes (diff) to the database and does not re generate the prisma client

📌 General tips

  1. When delete node_modules folder, prisma client also will be deleted. Prisma client usually located at node_modules/@prisma folder. Need to re generate it before use in the code. Better option would be add the following to the package.json
"scripts": {
    "postinstall": "prisma generate",
}

OR run the command manually,

$ pnpx prisma generate

📌 Connect from NestJS

Now, we have done the setup for database infrastructure. Let's focus on how do we connect NestJS with the created database.

First step is create a prisma module. (It is work just with a prisma service without having a prisma module. But, with the NestJS module philosophy, it is recommended to have a prisma module)

Then, prisma service,

$ nest generate module prisma
$ nest generate service prisma

Once module generated, module import automatically add to the imports list in the app.module. (this is not necessary. without import prisma module from here, it works as usual)

Prisma import to app module

Update src/prisma/prisma.service.ts file as bellow,

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}

Now, Prisma will be globally available for dependency injection from any services. Let's try that as well,

Let's say we have a user module. Then when we want to access prisma inside a user module, first, need to import prisma service in the user module:

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { PrismaService } from 'src/prisma/prisma.service';

@Module({
  controllers: [UserController],
  providers: [UserService, PrismaService],
})
export class UserModule {}

Then, can inject prisma service to the user service and use it within the user service,

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class UserService {
  // injecting prisma service to user service
  constructor(private prisma: PrismaService) {}

  async findAll(): Promise<User[]> {
    return this.prisma.user.findMany();
  }

  async findOne(id: number): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { id },
    });
  }

  async create(data: CreateUserDto): Promise<User> {
    return this.prisma.user.create({
      data,
    });
  }

  async update(id: number, data: UpdateUserDto): Promise<User> {
    return this.prisma.user.update({
      where: { id },
      data,
    });
  }

  async remove(id: number): Promise<User> {
    return this.prisma.user.delete({
      where: { id },
    });
  }
}

That's it 🏆

📌 GUI for Prisma

Prisma has awesome tool to visualise the data.

$ pnpx prisma studio

This will open the GUI on http://localhost:5555

📌 Conclusion

There are many ORM supports with NestJS. Drizzle and TypeORM are the other most popular candidates. I hear that Drizzle is much performant than others. All candidates have tradeoffs each other and I use Prisma for small and medium scale projects. It is easy to use when compare with TypeORM.

May be you can share your thoughts on this?

🤝 Let's meet again with another important topic

Cheers... Happy coding!!!