So far, in this series, we’ve been using TypeORM to manage our data and connect to our Postgres database. In this article, we look into Prisma, which is a capable alternative.
With Prisma, we describe our data using a Prisma schema file. It uses its own data modeling language and acts as a single source of truth. This differs from traditional ORMs that provide an object-oriented way of working with the database.
Every time we make changes to the schema file, we need to generate the Prisma client. In this process, Prisma parses our schema and creates a client along with all the TypeScript typings. This means that we no longer map SQL tables to model classes through the TypeScript code manually.
The Prisma client is a set of Node.js functions that we can call from within our code. Under the hood, the client connects to a query engine written in Rust.
Although it is possible to migrate from TypeORM to Prisma, in this aricle we set up our project from scratch.
You can find all of the code from this article in this repository.
Setting up PostgreSQL and Prisma
In this series, we are using PostgreSQL. The most straightforward way to add Postgres to our project is to do it with docker. In the second part of this series, we set up PostgreSQL with TypeORM. Let’s reuse the docker-compose file provided there:
1version: "3"
2services:
3 postgres:
4 container_name: nestjs-postgres
5 image: postgres:latest
6 ports:
7 - "5432:5432"
8 volumes:
9 - /data/postgres:/data/postgres
10 env_file:
11 - docker.env
12 networks:
13 - postgres
14
15 pgadmin:
16 links:
17 - postgres:postgres
18 container_name: nestjs-pgadmin
19 image: dpage/pgadmin4
20 ports:
21 - "8080:80"
22 volumes:
23 - /data/pgadmin:/root/.pgadmin
24 env_file:
25 - docker.env
26 networks:
27 - postgres
28
29networks:
30 postgres:
31 driver: bridgeWe also need to create a file that contains environment variables for our docker container.
1POSTGRES_USER=admin
2POSTGRES_PASSWORD=admin
3POSTGRES_DB=nestjs
4PGADMIN_DEFAULT_EMAIL=admin@admin.com
5PGADMIN_DEFAULT_PASSWORD=adminTo start using Prisma, we first need to install it.
1npm install prismaOnce that’s done, we can start using the Prisma CLI. The first thing we want to do is to initialize our configuration.
1npx prisma initDoing the above creates two files for us:
- schema.prisma: contains the database schema and specifies the connection with the database
- .env: contains environment variables
We need to modify the above .env file to adjust to the contents of docker.env:
1DATABASE_URL="postgresql://admin:admin@localhost:5432/nestjs?schema=public"Within the schema.prisma file, we have access to our environment variables. Let’s use the DATABASE_URL variable to set up the connection.
1datasource db {
2 provider = "postgresql"
3 url = env("DATABASE_URL")
4}
5
6generator client {
7 provider = "prisma-client-js"
8}Installing and using the Prisma Client
To start using the Prisma Client, we first need to install it.
1npm install @prisma/clientLet’s create a PrismaService class that initializes the PrismaClient.
1import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
2import { PrismaClient } from '@prisma/client';
3
4@Injectable()
5export class PrismaService extends PrismaClient
6 implements OnModuleInit, OnModuleDestroy {
7 async onModuleInit() {
8 await this.$connect();
9 }
10
11 async onModuleDestroy() {
12 await this.$disconnect();
13 }
14}We also need to create a module that exports the above service.
1import { Module } from '@nestjs/common';
2import { PrismaService } from './prisma.service';
3
4@Module({
5 imports: [],
6 controllers: [],
7 providers: [PrismaService],
8 exports: [PrismaService],
9})
10export class PrismaModule {}Managing basic tables with Prisma Client and Prisma Migrate
When we want to modify the structure of our database, we should create a migration. It consists of a SQL script that is supposed to make the necessary changes. Although we could write it by hand, Prisma can do it for us with the Prisma Migrate tool.
First, let’s define a simple Post model in our schema.
1// ...
2
3model Post {
4 id Int @default(autoincrement()) @id
5 title String
6 content String
7}Above, we define the basic set of fields for our model. Above we use just the Int and String types, but there are quite a few to choose from.
Aside from defining fields, we also can define attributes. Their job is to modify the behavior of fields or models.
With the @default attribute, we define a default value for a field. When using it, we have a set of functions to choose from. Above, we use autoincrement() to create a sequence of integers.
In our Post model, we also use the @id attribute. Thanks to that, our id field becomes the primary key.
Once we’ve got our model, we can create our migration.
1npx prisma migrate dev --name post --preview-featureAbove we use the --preview-feature flag, because the Prisma Migrate tool is still in preview. It means that the functionality was validated, but there might be small bugs.
Aside from creating a file with the SQL script, running the above command also changed our database.
Using Prisma Client to manage the data
Once all of the above is done, we can start using the Prisma Client. Let’s start with the basics.
1import { Injectable } from '@nestjs/common';
2import { PrismaService } from '../prisma/prisma.service';
3
4@Injectable()
5export class PostsService {
6 constructor(private readonly prismaService: PrismaService) {}
7
8 async getPosts() {
9 return this.prismaService.post.findMany();
10 }
11
12 // ...
13}The prismaService.post property was created when we generated our Prisma Client based on our schema and it recognizes the properties our Post entity has.
The findMany method without any arguments returns all entities from the queried collection.
1import { Injectable } from '@nestjs/common';
2import { PrismaService } from '../prisma/prisma.service';
3import { PostNotFoundException } from './exceptions/postNotFound.exception';
4
5@Injectable()
6export class PostsService {
7 constructor(private readonly prismaService: PrismaService) {}
8
9 async getPostById(id: number) {
10 const post = await this.prismaService.post.findUnique({
11 where: {
12 id,
13 },
14 });
15 if (!post) {
16 throw new PostNotFoundException(id);
17 }
18 return post;
19 }
20
21 // ...
22}Above, we use the findUnique method that aims to find a single entity from our collection. We need to provide it with the where property to narrow down the scope of our search.
We can also use the where property with the findMany method.
If the findUnique method returns undefined, we can assume that it didn’t find the entity. If that’s the case, we can throw an exception.
1import { NotFoundException } from '@nestjs/common';
2
3export class PostNotFoundException extends NotFoundException {
4 constructor(postId: number) {
5 super(`Post with id ${postId} not found`);
6 }
7}Doing the above results in responding with the 404 Not Found status code.
If you want to know more about error handling, check out API with NestJS #4. Error handling and data validation
1import { Injectable } from '@nestjs/common';
2import { PrismaService } from '../prisma/prisma.service';
3import { CreatePostDto } from './dto/createPost.dto';
4import { PostNotFoundException } from './exceptions/postNotFound.exception';
5
6@Injectable()
7export class PostsService {
8 constructor(private readonly prismaService: PrismaService) {}
9
10 async createPost(post: CreatePostDto) {
11 return this.prismaService.post.create({
12 data: post,
13 });
14 }
15
16 // ...
17}Above, we use the create method to save a new entity in the database. The CreatePostDto class describes all of the properties along with data validation.
1import { IsString, IsNotEmpty } from 'class-validator';
2
3export class CreatePostDto {
4 @IsString()
5 @IsNotEmpty()
6 title: string;
7
8 @IsString()
9 @IsNotEmpty()
10 content: string;
11}If you want to read about validation, check out API with NestJS #4. Error handling and data validation
1import { Injectable } from '@nestjs/common';
2import { PrismaService } from '../prisma/prisma.service';
3import { UpdatePostDto } from './dto/updatePost.dto';
4import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
5import { PrismaError } from '../utils/prismaError';
6import { PostNotFoundException } from './exceptions/postNotFound.exception';
7
8@Injectable()
9export class PostsService {
10 constructor(private readonly prismaService: PrismaService) {}
11
12 async updatePost(id: number, post: UpdatePostDto) {
13 try {
14 return await this.prismaService.post.update({
15 data: {
16 ...post,
17 id: undefined,
18 },
19 where: {
20 id,
21 },
22 });
23 } catch (error) {
24 if (
25 error instanceof PrismaClientKnownRequestError &&
26 error.code === PrismaError.RecordDoesNotExist
27 ) {
28 throw new PostNotFoundException(id);
29 }
30 throw error;
31 }
32 }
33
34 // ...
35}Above, we use the update method to modify an entity. It is worth noting that we ignore the id property so that the users can’t change it.
1import { IsString, IsNotEmpty, IsNumber } from 'class-validator';
2
3export class UpdatePostDto {
4 @IsNumber()
5 id: number;
6
7 @IsString()
8 @IsNotEmpty()
9 title: string;
10
11 @IsString()
12 @IsNotEmpty()
13 content: string;
14}We need to use the try...catch to see what errors have been thrown. To do that properly, we can create an enum based on the official documentation.
1export enum PrismaError {
2 RecordDoesNotExist = 'P2025',
3}1import { Injectable } from '@nestjs/common';
2import { PrismaService } from '../prisma/prisma.service';
3import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
4import { PrismaError } from '../utils/prismaError';
5import { PostNotFoundException } from './exceptions/postNotFound.exception';
6
7@Injectable()
8export class PostsService {
9 constructor(private readonly prismaService: PrismaService) {}
10
11 async deletePost(id: number) {
12 try {
13 return this.prismaService.post.delete({
14 where: {
15 id,
16 },
17 });
18 } catch (error) {
19 if (
20 error instanceof PrismaClientKnownRequestError &&
21 error.code === PrismaError.RecordDoesNotExist
22 ) {
23 throw new PostNotFoundException(id);
24 }
25 throw error;
26 }
27 }
28
29 // ...
30}Above with the delete method, we do a very similar thing in terms of error handling.
Once we’ve got our service ready, we can create a controller that uses it.
1import {
2 Body,
3 Controller,
4 Delete,
5 Get,
6 Param,
7 Patch,
8 Post,
9} from '@nestjs/common';
10import { PostsService } from './posts.service';
11import { CreatePostDto } from './dto/createPost.dto';
12import { UpdatePostDto } from './dto/updatePost.dto';
13import { FindOneParams } from '../utils/findOneParams';
14
15@Controller('posts')
16export default class PostsController {
17 constructor(private readonly postsService: PostsService) {
18 }
19
20 @Get()
21 async getPosts() {
22 return this.postsService.getPosts();
23 }
24
25 @Get(':id')
26 getPostById(@Param() { id }: FindOneParams) {
27 return this.postsService.getPostById(Number(id));
28 }
29
30 @Post()
31 async createPost(@Body() post: CreatePostDto) {
32 return this.postsService.createPost(post);
33 }
34
35 @Put(':id')
36 async updatePost(
37 @Param() { id }: FindOneParams,
38 @Body() post: UpdatePostDto,
39 ) {
40 return this.postsService.updatePost(Number(id), post);
41 }
42
43 @Delete(':id')
44 async deletePost(@Param() { id }: FindOneParams) {
45 return this.postsService.deletePost(Number(id));
46 }
47}It is worth noting that above we use the FindOneParams that transforms ids from strings to numbers.
1import { IsNumber } from 'class-validator';
2import { Transform } from 'class-transformer';
3
4export class FindOneParams {
5 @IsNumber()
6 @Transform(({ value }) => Number(value))
7 id: number;
8}Using multiple schema files
Putting all of our schema definitions into a single file might make it difficult to maintain. Unfortunately, Prisma currently does not support multiple schema files.
To deal with this issue, we can create a simple bash script that merges multiple schemas into one.
1{
2 "name": "nestjs-prisma",
3 "scripts": {
4 // ...
5 "generate-schema": "cat src/*/*.prisma > prisma/schema.prisma"
6 },
7 // ...
8}Running cat src/*/*.prisma > prisma/schema.prisma merges all .prisma files into the prisma/schema.prisma directory. It traverses through all of the subdirectories of src.
The cat command is available in Linux, Mac, and Windows Power Shell
We can also put the file schema.prisma into .gitignore so that we don’t have multiple sources of truth.
1# ...
2prisma/schema.prismaDoing the above allows us to keep separate schema files for specific modules. Let’s split our existing schema into multiple files:
1datasource db {
2 provider = "postgresql"
3 url = env("DATABASE_URL")
4}
5
6generator client {
7 provider = "prisma-client-js"
8}1model Post {
2 id Int @default(autoincrement()) @id
3 title String
4 content String
5}We end up with the following file structure:
1├── docker-compose.yml
2├── docker.env
3├── nest-cli.json
4├── package.json
5├── package-lock.json
6├── prisma
7│ ├── migrations
8│ │ ├── 20210327185422_post
9│ │ │ └── migration.sql
10│ │ └── migration_lock.toml
11│ └── schema.prisma
12├── README.md
13├── src
14│ ├── app.module.ts
15│ ├── main.ts
16│ ├── posts
17│ │ ├── dto
18│ │ │ ├── createPost.dto.ts
19│ │ │ └── updatePost.dto.ts
20│ │ ├── exceptions
21│ │ │ └── postNotFound.exception.ts
22│ │ ├── postSchema.prisma
23│ │ ├── posts.controller.ts
24│ │ ├── posts.module.ts
25│ │ └── posts.service.ts
26│ ├── prisma
27│ │ ├── baseSchema.prisma
28│ │ ├── prisma.module.ts
29│ │ └── prisma.service.ts
30│ └── utils
31│ ├── findOneParams.ts
32│ └── prismaError.ts
33├── tsconfig.build.json
34└── tsconfig.jsonSummary
In this article, we’ve learned the basics of Prisma. We’ve got to know its main principles and we’ve created a simple CRUD API with it. To do that, we’ve had to get familiar with the basics of the data modeling language Prisma uses. It also required us to learn about the basics of the Prisma Migrate tool. There is still quite a lot to learn about Prisma, so stay tuned!