Building a NestJS Rest API Using Prisma ORM
Learn how to build a NestJS REST API using Prisma ORM.
Join the DZone community and get the full member experience.
Join For FreeNestJS is a pretty solid framework for building backend applications.
In this 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.
In case you are new to NestJS, you can start with this post for starting with NestJS.
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, 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.
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 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.
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 theprisma
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 hostname and credentials. Take care not to commit this file to the source repository if it contains database credentials.
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 set up 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.
The 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.
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.
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.
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 shutdown mechanism where it destroys the database connection. Therefore, we implement the enableShutdownHooks()
method in the DBServic
e 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();
Creating the Application Service
Now that our database service is set up, we can create the actual application service. This service exposes methods to read, create, update and delete a book from the database.
See the 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, an 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.
Creating the REST API Controller
Finally, we can create a NestJS Controller to implement the REST API endpoints.
See the 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 the 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 post your views in the comments section. And if this post was helpful, please like and share it. This will help the post reach more people.
Published at DZone with permission of Saurabh Dashora. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments