DEV Community

Cover image for NestJs x Prisma: Made for each other
majiyd
majiyd

Posted on

NestJs x Prisma: Made for each other

TL/DR: Find the full code on github here

I started using NestJs and Prisma in 2021 and my life has never remained the same. When combined with Typescript, these tools would rapidly cut down development time.

NestJs is a progressive Node.js framework for building efficient, reliable and scalable server-side applications while Prisma is a next-generation Node.js and TypeScript ORM.

For those unfamiliar, an ORM or object-relational mapper (ORM) is an abstration layer that allows you to write sql queries in the programming language you're comfortable with it instead of SQL. It automates the transfer of data stored in relational database tables into objects that are more commonly used in application code

For example:

SELECT * FROM USERS WHERE zip_code=94107;
Enter fullscreen mode Exit fullscreen mode

would be written in with prisma as

const users = await prismaService.findMany({
  where: {
    zip_code: 94107
  }
})
Enter fullscreen mode Exit fullscreen mode

One thing you'd definitely love is the automatic type support and auto-suggest. These guarantee that you can never make invalid queries and also help you write queries faster.

Enough talk! Let's get into it. In this tutorial, we'll be creating a small REST api (GraphQL works too!) with a bunch of users and these users can put up items for rent.

Setting up NestJs

First, we setup nestjs by running

yarn global add @nestjs/cli @nestjs/schematics
nest new rently # our new nestjs app
Enter fullscreen mode Exit fullscreen mode

This would bootstrap a NestJs app. You'd also find some files created. These are the root module, contoller and test files. You can find out more about them here but for now, I don't think you should worry too much about it.

Nest-js-default-files

Let's run the application in dev mode with

yarn start:dev
Enter fullscreen mode Exit fullscreen mode

Our app is running fine!

nest-app-running

Now, let's head to postman and send a get request to localhost:3000 and we should see this!

hello world from nest

Let's go back to the app module and talk a little about the files generated. In summary, the controller picks up the http requests

app-controller

while the service files handles the actual logic e.g database queries, computations, communicating with external services, etc.

app-service

Think of the module file as the point of entry or the registration point for external modules used within the service.

Exciting!

Now we're going to create our first module called users. With this module we should be able to create a new user, get a single user and get all users.

You can create the files for the user module with nest g service users && nest g controller users && nest g module users

You should see something like this
user-module

To access the /users on postman we need to register UsersModule in AppModule like so:

...
import { UsersModule } from './users/users.module';

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

Enter fullscreen mode Exit fullscreen mode

You might notice that nest already registered the module for you. Pretty neat!

Let's test our new endpoint by modifying users.service

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  findAll(): string {
    return 'returns all users';
  }
}

Enter fullscreen mode Exit fullscreen mode

Next, we update users.controllers by importing and adding UsersService to the list of constructors

...
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

}

Enter fullscreen mode Exit fullscreen mode

Finally, you can use the service like so

import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Get()
  findAll(): string {
    return this.usersService.findAll();
  }
}

Enter fullscreen mode Exit fullscreen mode

Let's test all we've done by hitting http:localhost:3000/users

nest-users
Yay! It works!

In reality, we'd like to return actual data of the users. This is where Prisma comes in.

Setting up Prisma

Let's setup Prisma by running

yarn add prisma --dev

#init source provider
npx prisma init --datasource-provider sqlite
Enter fullscreen mode Exit fullscreen mode

The datasource provider could be postgres, mysql or anything else but in our case we would be using sqlite.

The next bit is to set up our schema (this can also be seen as our database model). The last command we ran should have created a file in prisma/schema.prisma. Navigate to this file, you should see somethig like

...

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

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


Enter fullscreen mode Exit fullscreen mode

These are all fine and default code. It tells Prisma about what kind of DB we're using etc.

Now, we'll create our first model, the user model by adding the following block of code to our schema file

...

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?
}
Enter fullscreen mode Exit fullscreen mode

A lot of things in Prisma are pretty intuitive and self-explanatory. The model name corresponds to the the table name in the db, the first column refers to the column names, the second column specifies the data type while the third column specifies additional information such as default values or relations.
@id means default id or primary key if you like, @default means a default value would be added. autoincrement means that the primary key would automatically be incremented when there's a new entry into the database. now and @updated at are used to assign default time stamps to database entries.

Next we run yarn prisma generate to generate our models, we'd also run yarn prisma db push to keep our database in sync with our schema. You should see your db created if you're using sqlite like this

yarn prisma db push

In order to manage changes to our database models, it makes sense to create migrations to keep track of incremental changes to our db. You can create a migration by running

yarn prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

enter "y", then a name for your migration
prisma migration

You should seen the migration file created. Let's take a look at the migration file to see and idea of what it looks like.

sql of migration file

It's pretty much an sql file specifying the changes or in this case how to create the user table.

Woah! that's was a lot!

Now, before we can use our models in any of our services, we first need to create a Prisma service file, thus we'd be able to inject Prisma as a service to any module.

nest g service prisma
Enter fullscreen mode Exit fullscreen mode

navigate to prisma.service.ts then replace the entire with the following block


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

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

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

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

Next, update main.ts

...
import { PrismaService } from './prisma/prisma.service';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const prismaService = app.get(PrismaService);
  await prismaService.enableShutdownHooks(app);

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Now, we have our Prisma service up and running.

Next, we need to add PrismaService to the list of providers for the module then initialize PrismaService in the constructor of UsersService or any other service as needed like so

// user.module

...
import { PrismaService } from 'src/prisma/prisma.service';

@Module({
  providers: [UsersService, PrismaService],
  controllers: [UsersController],
})
export class UsersModule {}


Enter fullscreen mode Exit fullscreen mode
...
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class UsersService {
  constructor(private readonly prismaService: PrismaService) {}

  ...
}
Enter fullscreen mode Exit fullscreen mode

now we can use Prisma like this:

...
async findAll() {
    return await this.prismaService.user.findMany();
}
...
Enter fullscreen mode Exit fullscreen mode

Voila! I'm sure you really enjoyed the type support you got.

In the end, the entire file should look something like this

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

@Injectable()
export class UsersService {
  constructor(private readonly prismaService: PrismaService) {}

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

Enter fullscreen mode Exit fullscreen mode

Don't for get update the type in the controller file

import { Controller, Get } from '@nestjs/common';
import { User } from '@prisma/client';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Get()
  async findAll(): Promise<User[]> {
    return await this.usersService.findAll();
  }
}

Enter fullscreen mode Exit fullscreen mode

and there we have it

successfully-getting-all-users

Creating Data

Next, let's try to create new users.

// users.conntroller
@Controller('users')
export class UsersController {
  ...

  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    this.usersService.create(createUserDto);
  }
}

Enter fullscreen mode Exit fullscreen mode

You'd notice something new CreateUserDto. Dtos are used to specify the structure of data to expect while @body implies that the data should be found in the body of the post request, they can also be used to along side class-validator library for validation.

First, we install some packages

yarn add class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode

Add validation support to our app like so

// main.ts
import { ValidationPipe } from '@nestjs/common';
...

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  const prismaService = app.get(PrismaService);
  await prismaService.enableShutdownHooks(app);

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Next, we create a new file called create-user.dto.ts in the users folder.

Now let's add the following block to the file

import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  name: string;

  @IsEmail()
  email: string;
}

Enter fullscreen mode Exit fullscreen mode

Notice that this matches our schema from schema.prisma?

Anyway, now we have CreateUserDto. The next bit is to update user.service to handle the creation.

...
import { Prisma, User } from '@prisma/client';
...
import { CreateUserDto } from './create-user.dto';

@Injectable()
export class UsersService {
  constructor(private readonly prismaService: PrismaService) {}

  ...

  async create(createUserData: Prisma.UserCreateInput): Promise<User> {
    const { name, email } = createUserData;
    return await this.prismaService.user.create({
      data: {
        name,
        email,
      },
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

With this we should be able to create a new user with validation added!

post-data-with-nest-js-with-validation

class-validator-error-validation-with-nest-js

Getting single data from the db

Next, we're going to try to get a single user from the db by its id. Remember that every user in our db has a unique id which is also the primary key of the user table.

let's say we want the url localhost:3000/users/3 to mean get the user with id=3, we'd do the following:

First, update users.controller with the following block

import {
  ...
  Param,
  ParseIntPipe,
  Post,
} from '@nestjs/common';
...

@Controller('users')
export class UsersController {
  ...

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number): Promise<User | null> {
    return await this.usersService.findUserById(id);
  }

  ...
}

Enter fullscreen mode Exit fullscreen mode

Couple of interesting new things here, id specificies the param of interest, ParseIntPipe is a built-in pipe for validating numbers.

Next, we update users.service to include findUserById.

import { Injectable, NotFoundException } from '@nestjs/common';
...

@Injectable()
export class UsersService {
  ...

  async findUserById(id: number): Promise<User | null> {
    const user = await this.prismaService.user.findUnique({
      where: {
        id,
      },
    });

    if (!user) {
      // optional, you can return null/undefined depending on your use case
      throw new NotFoundException();
    }

    return user;
  }

  ...
}

Enter fullscreen mode Exit fullscreen mode

NotFoundException is one of the inbuilt exceptions in nestjs, it'll return this by default

NotFoundException

Anyway, our code works!

successfully getting single item nest prisma

And the best part is that validation works out of the box!

Relation in prisma

Let's create a new model item, users should be able to add and update items.

First, let's update schema.prisma with our new model

...

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?

  items Item[] // one-to-many relation, i.e a user can have multiple items

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?
}

model Item {
  id        Int      @id @default(autoincrement())
  name      String
  isForRent Boolean? @default(false)

  User   User? @relation(fields: [userId], references: [id])
  userId Int? // id for user who is associated with this item

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?
}

Enter fullscreen mode Exit fullscreen mode

Don't forget to create the migration with yarn prisma migrate dev.

Now let's create the controller, module and service files for item. Remembers this boring old command from before nest g service users && nest g controller users && nest g module users? Well, there's a better way to do it.

nest g resource
Enter fullscreen mode Exit fullscreen mode

This would help you generate all the files you need including Dtos and test files.

nest g resource
We selected rest api for our case here but it can be any of the options

Now, let's create our items.

The last command we ran should have created a file called create-item.dto.ts. If it did not, create one. Now, we update this file to look like this

import { IsBoolean, IsInt, IsNotEmpty, IsOptional } from 'class-validator';

export class CreateItemDto {
  @IsNotEmpty()
  name: string;

  @IsOptional()
  @IsBoolean()
  isForRent: boolean;


  // In this example we want the user id to be part of the payload. This isn't always the case
  @IsInt()
  userId: number;
}

Enter fullscreen mode Exit fullscreen mode

Notice how self-explanatory class-validator is?

To use Prisma in items.service we have to add PrismaService to the list of providers in ItemsModule. Update items.module to look like this

// items.module.ts

import { Module } from '@nestjs/common';
import { ItemsService } from './items.service';
import { ItemsController } from './items.controller';
import { PrismaService } from 'src/prisma/prisma.service';

@Module({
  controllers: [ItemsController],
  providers: [ItemsService, PrismaService],
})
export class ItemsModule {}

Enter fullscreen mode Exit fullscreen mode

items.controller.ts should already have the block below, if not add it

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { ItemsService } from './items.service';
import { CreateItemDto } from './dto/create-item.dto';
import { UpdateItemDto } from './dto/update-item.dto';

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Post()
  async create(@Body() createItemDto: CreateItemDto) {
    return this.itemsService.create(createItemDto);
  }

 ...
}


Enter fullscreen mode Exit fullscreen mode

Now the most important part, the actual creation logic in the service file. Let's do that by adding PrismaService to the constructor of ItemsService

import { Injectable } from '@nestjs/common';
import { Item } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateItemDto } from './dto/create-item.dto';
import { UpdateItemDto } from './dto/update-item.dto';

@Injectable()
export class ItemsService {
  constructor(private readonly prismaService: PrismaService) {}

  async create(createItemData: CreateItemDto): Promise<Item> {
  ...
}
  ...
}

Enter fullscreen mode Exit fullscreen mode

There are three ways to add relation data and the all work to the best of knowledge.

// 1
async create(createItemData: CreateItemDto): Promise<Item> {
    const { name, isForRent, userId } = createItemData;
    const user = await this.prismaService.user.update({
      where: {
        id: userId,
      },
      data: {
        items: {
          create: {
            name,
            isForRent,
          },
        },
      },
      include: {
        items: {
          take: -1,
        },
      },
    });

    return user.items[0];
  }
Enter fullscreen mode Exit fullscreen mode
// 2

async create(createItemData: CreateItemDto): Promise<Item> {
    const { name, isForRent, userId } = createItemData;
    const item = await this.prismaService.item.create({
      data: {
        name,
        isForRent,
        User: {
          connect: {
            id: userId,
          },
        },
      },
    });

    return item;
  }
Enter fullscreen mode Exit fullscreen mode
// 3

async create(createItemData: CreateItemDto): Promise<Item> {
    const { name, isForRent, userId } = createItemData;
    const item = await this.prismaService.item.create({
      data: {
        name,
        isForRent,
        userId,
      },
    });

    return item;
  }

Enter fullscreen mode Exit fullscreen mode

I personally prefer the first method but it's up to you to choose what you prefer.

successfully entering related data with prisma on nest js

Getting related data

Let's say we want to get all the items connected to a certain userId. and also let's say we want the url to be something like this: http://localhost:3000/items?userId=1

First we'd update the controller to include the following block

// items.controller.ts

import {
  ...
  ParseIntPipe,
} from '@nestjs/common';
...

@Controller('items')
export class ItemsController {

  ...

  @Get()
  async findAllByUserId(@Query('userId', ParseIntPipe) userId: number) {
    return this.itemsService.findAllByUserId(userId);
  }

  ...
}

Enter fullscreen mode Exit fullscreen mode

Next, let's add our query to ItemsService.

...
import { Item, Prisma } from '@prisma/client';
...

@Injectable()
export class ItemsService {
  ...

  async findAllByUserId(userId: number): Promise<Item[]> {
    return await this.prismaService.item.findMany({
      where: {
        userId,
      },
    });
  }

 ...
}

Enter fullscreen mode Exit fullscreen mode

And it works!

nest-prisma-get-items-by-id

Updating things

Finally, we're going to update an item. Add the following block to items.controller

import {
  ...
  ParseIntPipe,
  Put,
} from '@nestjs/common';
import { UpdateItemDto } from './dto/update-item.dto';
...

@Controller('items')
export class ItemsController {
  ...

  @Put(':id')
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateItemDto: UpdateItemDto,
  ) {
    return this.itemsService.update(id, updateItemDto);
  }

  ...
}

Enter fullscreen mode Exit fullscreen mode

UpdateItemDto should also have been generated by nest g resource and it should be something like this

// update-item.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateItemDto } from './create-item.dto';

export class UpdateItemDto extends PartialType(CreateItemDto) {}

Enter fullscreen mode Exit fullscreen mode

In the service file, we add the following

// items.service.ts
...
import { Item } from '@prisma/client';
..

@Injectable()
export class ItemsService {
  ...

  async update(id: number, updateItemDto: UpdateItemDto): Promise<Item> {
    const { name, isForRent } = updateItemDto;
    return await this.prismaService.item.update({
      where: {
        id,
      },
      data: {
        name,
        isForRent,
      },
    });
  }

  ...
}

Enter fullscreen mode Exit fullscreen mode

And we're done, notice how we're building endpoints and services faster now? That's just the power of NestJs and Prisma together.

Anyway, we're done!

nest-create-item

nest-updating-item-with-prisma

Feel free to play around with the apis until you get really comfortable with it. Do let me know how much you've enjoyed using these tools in the comment.

Useful links

  1. Github repo
  2. https://nestjs.com/
  3. https://www.prisma.io/
  4. https://www.fullstackpython.com/object-relational-mappers-orms.html

Top comments (2)

Collapse
 
ruheni profile image
Ruheni Alex

Hi Majiyd πŸ‘‹

Alex here from the Prisma team. Amazing work on the article.

Quick note/ update: Prisma 5 shipped with a fairly small breaking change that removed the beforeExit hook. Here's a snippet with the changes that you would have to make to the PrismaService.

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

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

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

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

One other minor change: in order to make sure the application shutdown hooks are used, you can register the hook in the main.ts file within your app:

// main.ts
import { ValidationPipe } from '@nestjs/common';
...

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
-  const prismaService = app.get(PrismaService);
-  await prismaService.enableShutdownHooks(app);
+  app.enableShutdownHooks();

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
 
otarbes profile image
otarbes

Hi Majiyd, fantastic article on the power of combining NestJS, Prisma, and TypeScript! We can definitely relate to how these tools can greatly improve development efficiency.

In line with that, we'd be really interested to hear your thoughts on our open-project, Traxion an open-source generative toolkit, based on Prisma and NestJS. We'd be honored if you could give Traxion a try and share your feedback with us.

Traxion is designed to drastically accelerate NestJS projects while maintaining full control over your code. It offers a range of features such as data management with Prisma, instant GraphQL API based on Prisma Schema, Role-Based Access Control, and official packages including Dev-Kit, Nest-Authentication, Nest-Authorization, and Nest-Utilities.

Your insights will help us make Traxion even more valuable for the NestJS community. Check it out at github.com/tractr/traxion and let us know your thoughts! Happy coding! Cheers ! 😊