Building a NestJS REST API using Prisma ORM

Building a NestJS REST API using Prisma ORM

The new ORM that is making some serious waves!

NestJS is a pretty solid framework for building backend applications. We talked about starting with a NestJS project in this earlier post.

In today’s post, we are going to take our NestJS knowledge and combine it with Prisma to build a RESTful API. This post is going to have a bunch of code that you can get from the Github link at the end or you can also code along.

1 - What is Prisma?

Prisma is a next-generation Object Relational Mapper (ORM). We can use it with Typescript as well as Javascript. It takes a somewhat different approach to traditional ORMs. You can check out Prisma’s official website for more information.

Instead of classes, Prisma uses a special Schema Definition Language.

Basically, developers describe their schemas using this Schema Definition Language. Prisma runs over the schemas and writes the appropriate migrations depending on the chosen database. It also generates type-safe code to interact with the database.

In other words, Prisma provides an alternative to writing plain SQL queries or using other ORMs (such as TypeORM or Sequelize). It can work with various databases such as PostgreSQL, MySQL, SQLite and even MongoDB.

Prisma consists of two main parts:

  • Prisma Migrate – This is the migration tool provided by Prisma. It helps us keep our database schema in sync with the Prisma schema. For every change to our schema, the Prisma Migrate generates a migration file. In this way, it also helps maintain a history of all changes that may happen to our schema.

  • Prisma Client – This is the auto-generated query builder. The Prisma Client acts as the bridge between our application code and the database. It also provides type-safety.

We will utilize both Prisma Migrate and Prisma Client to build our application.

Subscribe to the Publication

2 - How good is Prisma ORM?

While this might be a subjective question, Prisma aims to make it easy for developers to deal with database queries.

As any developer will know, it is absolutely necessary for most applications to interact with databases to manage data. This interaction can occur using raw queries or ORM frameworks (such as TypeORM). While raw queries or query builders provide more control, they reduce the overall developer productivity.

On the other hand, ORM frameworks abstract the SQL by defining the database model as classes. This increases productivity but drastically reduces developer-control. It also leads to object-impedance mismatch.

Object Impedance Mismatch is a conceptual problem that arises when an object oriented programming language interacts with a relational database. In a relational database, data is normalized and links between different entities is via foreign keys. However, objects establish the same relation using nested structures. Developers writing application code are used to thinking about objects and their structure. This causes a mismatch when dealing with a relational database. Prisma attempts to solve the issues around object relational mapping by making developers more productive while giving them more control.

Some important ways of how Prisma achieves this are as follows:

  • It allows developers to think in terms of objects.

  • It helps avoid complex model objects

  • It helps maintain a single source of truth for both database and application using schema files

  • Facilitates writing type-safe queries to catch errors during compile time

  • Less boilerplate code. Developers can simply define their schemas and not worry about specific ORM frameworks.

3 - Setting up a NestJS Prisma Project

With a basic understanding of Prisma, we can now start to build our NestJS Prisma REST API.

As the first step, we create a new NestJS Project using the Nest CLI. The below command creates a new working NestJS project.

$ nest new nestjs-prisma-demo-app
$ cd nestjs-prisma-demo-app

In the next step, we install Prisma.

$ npm install prisma --save-dev

Note that this is only a development dependency.

We will be using the Prisma CLI to work with Prisma. To invoke the CLI, we use npx as below.

$ npx prisma

Once Prisma is activated, we create our initial Prisma setup.

$ npx prisma init

This command creates a new directory named prisma and a configuration file within our project directory.

  • schema.prisma – This is the most important file from Prisma’s perspective. It will be present within the prisma directory. It specifies the database connection and also contains the database schema. You could think of it as the core of our application.
  • .env – This is like an environment configuration file. It stores the database host name and credentials. Take care not to commit this file to source repository if it contains database credentials.

4 - Prisma Database Connection Setup

For our demo NestJS Prisma application, we will be using SQLite as our database. This is an easy-to-use database option since SQLite stores data in files. As a result, we don’t have to setup database servers.

To configure our database connection, we have to make changes in the schema.prisma file.

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

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

Basically, we have to change the provider in datasource db section to sqlite. By default, it uses postgres, but you can also use other database solutions.

Second change is in the .env file.

DATABASE_URL="file:./test.db"

Here, we specify the path to our database file. The file name is test.db. You can name it anything you want.

5 - Prisma Migrate Command

Now, we are ready to create tables in our SQLite database.

To do so, we will first write our schema. As discussed earlier, the schema is defined in the schema.prisma file we saw a moment ago.

We will update the same file as below:

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

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

model Book {
  id  Int @default(autoincrement()) @id
  title String
  author String
  publishYear Int
}

As you can see, we have used the schema definition language to create a model named Book. It has a few basic attributes such as title, author and the publishYear. The id is an integer and is set to auto-increment.

This schema is the single source of truth for our application and also the database. No need to write any other classes as we have to do for other ORMs such as TypeORM. More on that in a future post.

We will now generate the migration files using Prisma Migrate.

$ npx prisma migrate dev --name init

Basically, this command generates SQL files and also runs them on the configured database. You should be able to find the below SQL file within the prisma/migrations directory in your project.

CREATE TABLE "Book" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title" TEXT NOT NULL,
    "author" TEXT NOT NULL,
    "publishYear" INTEGER NOT NULL
);

Also, the database file test.db will be automatically created.

6 - Installing and Generating Prisma Client

Our database is now ready. However, we still don’t have a way to interact with the database from our application.

This is where the Prisma Client comes into the picture.

But what is Prisma Client?

Prisma Client is a type-safe database client to interact with our database. It is generated using the model definition from our prisma.schema file. In other words, the client exposes CRUD operations specific to our model.

We can install Prisma Client using the below command.

$ npm install @prisma/client

Under the hood, the installation step also executes the prisma generate command. In case we make changes to our schema (like adding a field or a new model), we can simply invoke prisma generate command to update our Prisma Client accordingly.

Once the installation is successful, the Prisma Client library in node_modules/@prisma/client is updated accordingly.

7 - Creating the Database Service

It is not good practice to directly work with the Prisma Client API in our core application. Therefore, we will abstract away the Prisma Client API within another service.

See below code from the db.service.ts file.

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

@Injectable()
export class DBService extends PrismaClient implements OnModuleInit {

    async onModuleInit() {
        await this.$connect();
    }

    async enableShutdownHooks(app: INestApplication) {
        this.$on('beforeExit', async () => {
            await app.close();
        })
    }
}

Basically, this is our application’s database service that extends the PrismaClient we generated in the previous step. It implements the interface OnModuleInit. If we don’t use OnModuleInit, Prisma connects to the database lazily.

Prisma also has its own shut down mechanism where it destroys the database connection. Therefore, we implement the enableShutdownHooks() method in the DBService and also call it in the main.ts file.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DBService } from './db.service';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const dbService: DBService = app.get(DBService);
  dbService.enableShutdownHooks(app)
  await app.listen(3000);
}
bootstrap();

8 - Creating the Application Service

Now that our database service is setup, we can create the actual application service. This service exposes methods to read, create, update and delete a book from the database.

See below code from the file named book.service.ts:

import { Injectable } from "@nestjs/common";
import { Book, Prisma } from "@prisma/client";
import { DBService } from "./db.service";

@Injectable()
export class BookService {

    constructor(private dbService: DBService) {}

    async getBook(id: Prisma.BookWhereUniqueInput): Promise<Book | null> {
        return this.dbService.book.findUnique({
            where: id
        })
    }

    async createBook(data: Prisma.BookCreateInput): Promise<Book> {
        return this.dbService.book.create({
            data,
        })
    }

    async updateBook(params: {
        where: Prisma.BookWhereUniqueInput;
        data: Prisma.BookUpdateInput;
    }): Promise<Book> {
        const { where, data } = params;
        return this.dbService.book.update({
            data,
            where,
        });
    }

    async deleteBook(where: Prisma.BookWhereUniqueInput): Promise<Book> {
        return this.dbService.book.delete({
            where,
        });
    }
}

This is a standard NestJS Service where we inject an instance of the DBService. However, important point to note is the use of Prisma Client’s generated types such as BookCreateInput, BookUpdateInput, Book etc to ensure that the methods of our service are properly typed. There is no need to create any additional DTOs or interfaces to support type-safety.

9 - Creating the REST API Controller

Finally, we can create a NestJS Controller to implement the REST API endpoints.

See below code from the file named book.controller.ts.

import { Body, Controller, Delete, Get, Param, Post, Put } from "@nestjs/common";
import { Book } from "@prisma/client";
import { BookService } from "./book.service";

@Controller()
export class BookController {
    constructor(
        private readonly bookService: BookService
    ) {}

    @Get('books/:id')
    async getBookById(@Param('id') id: string): Promise<Book> {
        return this.bookService.getBook({id: Number(id)});
    }

    @Post('books')
    async createBook(@Body() bookData: {title: string, author: string, publishYear: Number}): Promise<Book> {
        const { title, author } = bookData;
        const publishYear = Number(bookData.publishYear);
        return this.bookService.createBook({
            title,
            author,
            publishYear
        })
    }   

    @Put('books/:id')
    async updateBook(@Param('id') id: string, @Body() bookData: {title: string, author: string, publishYear: Number}): Promise<Book> {
        const { title, author } = bookData;
        const publishYear = Number(bookData.publishYear);

        return this.bookService.updateBook({
            where: {id: Number(id)},
            data: {
                title, 
                author,
                publishYear
            }
        })
    }

    @Delete('books/:id') 
    async deleteBook(@Param('id') id: string): Promise<Book> {
        return this.bookService.deleteBook({
            id: Number(id)
        })
    }
}

Here also, we use the Book type generated by the Prisma Client to implement type-safety.

As a last step, we configure the controllers and services in the App Module (app.module.ts file) so that NestJS can discover them during application startup.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BookController } from './book.controller';
import { BookService } from './book.service';
import { DBService } from './db.service';

@Module({
  imports: [],
  controllers: [AppController, BookController],
  providers: [AppService, BookService, DBService],
})
export class AppModule {}

Our application is now ready. We can start the application using npm run start and test the various endpoints at http://localhost:3000/books.


With this, we have successfully created a NestJS REST API using Prisma ORM. We started from the very basics of Prisma and worked our way to writing the schema, generating a migration and a client to interact with our database tables.

The code for this post is available on Github in case you want to study it more closely.

How did you find this post? Have you already started using Prisma ORM in your projects? If yes, how do you find it in terms of usability? Or are you using other ORM frameworks in your projects?

I would love to hear your thoughts on this matter. So, please do post your views in the comments section. And if this post was helpful, please do like and share it. This will help the post reach more people.

Did you find this article valuable?

Support Saurabh Dashora by becoming a sponsor. Any amount is appreciated!