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;
would be written in with prisma as
const users = await prismaService.findMany({
where: {
zip_code: 94107
}
})
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
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.
Let's run the application in dev mode with
yarn start:dev
Our app is running fine!
Now, let's head to postman and send a get request to localhost:3000
and we should see this!
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
while the service files handles the actual logic e.g database queries, computations, communicating with external services, etc.
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
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 {}
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';
}
}
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) {}
}
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();
}
}
Let's test all we've done by hitting http:localhost:3000/users
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
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")
}
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?
}
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
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 "y"
, then a name for your 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.
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
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();
});
}
}
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();
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 {}
...
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class UsersService {
constructor(private readonly prismaService: PrismaService) {}
...
}
now we can use Prisma
like this:
...
async findAll() {
return await this.prismaService.user.findMany();
}
...
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();
}
}
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();
}
}
and there we have it
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);
}
}
You'd notice something new CreateUserDto
. Dto
s 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
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();
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;
}
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,
},
});
}
}
With this we should be able to create a new user with validation added!
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);
}
...
}
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;
}
...
}
NotFoundException
is one of the inbuilt exceptions in nestjs, it'll return this by default
Anyway, our code works!
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?
}
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
This would help you generate all the files you need including Dto
s and test files.
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;
}
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 {}
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);
}
...
}
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> {
...
}
...
}
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];
}
// 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;
}
// 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;
}
I personally prefer the first method but it's up to you to choose what you prefer.
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);
}
...
}
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,
},
});
}
...
}
And it works!
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);
}
...
}
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) {}
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,
},
});
}
...
}
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!
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.
Top comments (2)
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 thePrismaService
.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:Cheers!
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 ! ๐