Scope of discussion:
- What we want to build
- Nest JS installation + new project initialization
- Nest JS overview
- Prisma configuration
- Validation configuration
- Create user endpoints (register, login, update, delete)
- Password encryption
I have been looking for a great, nice, superb Node JS framework to build an API. After doing some exploration I found Nest JS which is super great. It's a Node JS framework with many built-in features written in Typescript. Check Nest JS documentation for more details Nest JS Doc.
In this article, I'm going to show how to create a REST API with these features:
- CRUD (Create Read Update Delete) with Prisma,
- Validation,
- Authentication,
- Route protection,
- Handle multiple .env files,
- Unit test,
- e2e test,
- Open API for documentation, etc.
Full code of the entire series can be accessed here: https://github.com/alfism1/nestjs-api
Okay, now let's get started.
First thing first, let's create a new Nest JS project called superb-api.
$ npm i -g @nestjs/cli
$ nest new superb-api
It will prompt a question to choose package manager (npm, yarn, or pnpm). Choose whichever you want.
Once the project initialization is completed, go to the project folder, open the code editor, and run it.
$ cd superb-api
$ npm run start
First App Run
By default, the Nest JS use 3000 as the default port.
If you want to change the port number, just open up src/main.ts
and update the port number await app.listen(ANY_AVAILABLE_PORT_NUMBER);
.
Nest JS Overview
Now, let's do a quick overview of the project folder structure.
Most of our codes will be written in the src
folder. Let's take a look at the src
folder. There are 5 files:
-
app.controller.ts
: A basic controller with a single route. -
app.controller.spec.ts
: The unit tests for the controller. -
app.module.ts
: The root module of the application. -
app.service.ts
: A basic service with a single method. -
main.ts
: The entry file of the application which uses the core function NestFactory to create a Nest application instance.
Controller
Controllers are responsible for handling incoming requests and returning responses. As per the Nest JS rules, controller will be written in a class with @Controller()
decorator. We can also add a string as the prefix inside the controller decorator @Controller('some-prefix')
then the URL will be domain.com/some-prefix
. Inside the controller class, there will be functions with HTTP methods decorator @GET()
, @POST()
, @PUT()
, @PATCH()
, @DELETE()
. Similar to the controller, HTTP methods decorator can also receive a string, for example @GET('test')
, and then the endpoint URL will be domain.com/some-prefix/test
.
import { Controller, Get } from '@nestjs/common';
// Import the AppService class, which is defined in the app.service.ts file.
import { AppService } from './app.service';
// Controller decorator, which is used to define a basic route.
@Controller()
export class AppController {
// Get decorator, which is a method decorator that defines a basic GET route.
@Get()
getHello(): string {
// controller process goes here...
}
}
For more details about the controller, check controller documentation.
Service/Provider
Services or Providers are responsible for handling the main process. We're gonna write all the endpoint logic here. As per the Nest JS rules, provider will be written in a class with @Injectable()
decorator.
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
In the code above, we have getHello
method which is responsible for returning a string 'Hello World!'. Then we can inject this provider into the controller
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
// inject the AppService into the AppController
constructor(private readonly appService: AppService) {}
@Get('test')
getHello(): string {
// call the AppService's getHello method
return this.appService.getHello();
}
}
For more details about the service or provider, check provider documentation.
Module
In NestJS, a module is a class annotated with the @Module()
decorator. It is used to organize code into cohesive blocks of functionality. Modules have a few key responsibilities, such as defining providers (services, factories, repositories, etc.), controllers, and other related stuff. They also allow for defining imports and exports, which are used to manage dependencies between different parts of the application.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
For more details about the service or provider, check module documentation.
Let's build our superb API
Prisma installation
We're gonna use Prisma as the database ORM. Prisma is a great library for working with databases. It supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB and CockroachDB.
First, let's install Prisma package in our project
$ npm install prisma --save-dev
Once prisma is installed, run the command below
$ npx prisma init
It will create schema.prisma
file inside prisma
folder
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
and .env
file in the root folder
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
If you haven't installed PostgreSQL, just download it.
Make sure to set the correct DATABASE_URL
in .env
file.
Next, open up the schema.prisma
and let's define the model schema
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean? @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
Once the schema is created, sync the schema to the database by executing
npx prisma migrate dev --name init
and check the database. It will create these tables:
Install prisma client
npm install @prisma/client
Note that during installation, Prisma automatically invokes the
prisma generate
command for you. In the future, you need to run this command after every change to your Prisma models to update your generated Prisma Client.The
prisma generate
command reads your Prisma schema and updates the generated Prisma Client library insidenode_modules/@prisma/client
Next, let's create a new file called prisma.service.ts
inside src/core/services/
directory:
// src/core/services/prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
For the last configuration of Prisma, let's register PrismaService
as a global module so it can be used on any other modules. Create a new file called core.module.ts
inside src/core/
// src/core/core.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './services/prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class CoreModule {}
then update our app.module.ts
by adding CoreModule
:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './modules/users/users.module';
import { CoreModule } from './core/core.module';
@Module({
imports: [UsersModule, CoreModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Great! Now we're ready to interact with the database :)
Validation config
Nest JS can do request body and query validation very well (more details check here).
First, install the packages:
$ npm i --save class-validator class-transformer
$ npm install @nestjs/mapped-types
and update our main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global pipe that will be applied to all routes
// This will validate the request body against the DTO
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
We added app.useGlobalPipes(new ValidationPipe());
in main.ts
to make it globally applied.
Create users endpoints
Let's move on to the endpoint creation.
In the src
folder, create a new folder called modules/users
.
Inside modules/users
, we'll create endpoints that handle any logic related to user model such as; create user, update user, and delete user.
Before diving into the code, let's install some packages:
bcrypt
We'll encrypt the user's password during the user creation.
$ npm i bcrypt
$ npm install --save @types/bcrypt
Now let's create a new folder src/modules/users/dtos
and create these files:
- create-user.dto.ts Validation for user creation/registration
import { IsEmail, IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
password: string;
@IsNotEmpty()
name: string;
}
- update-user.dto.ts Validation for user update
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUsertDto extends PartialType(CreateUserDto) {}
- login-user.dto.ts Validation for login
import { IsEmail, IsNotEmpty } from 'class-validator';
export class LoginUserDto {
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
password: string;
}
Let's start with creating a new controller boilerplate called users.controller.ts
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUsertDto } from './dtos/update-user.dto';
import { LoginUserDto } from './dtos/login-user.dto';
@Controller('users')
export class UsersController {
@Post('register')
registerUser(@Body() createUserDto: CreateUserDto): string {
console.log(createUserDto);
return 'Post User!';
}
@Post('login')
loginUser(@Body() loginUserDto: LoginUserDto): string {
console.log(loginUserDto);
return 'Login User!';
}
@Get('me')
me(): string {
return 'Get my Profile!';
}
@Patch(':id')
updateUser(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUsertDto,
): string {
console.log(updateUserDto);
return `Update User ${id}!`;
}
@Delete(':id')
deleteUser(@Param('id', ParseIntPipe) id: number): string {
return `Delete User ${id}!`;
}
}
users.controller.ts
has five endpoints:
-
POST /users/register
for user registration process, -
POST /users/login
for user login process, -
GET /users/me
to Get the user's profile, -
PATCH /users/:id
for user updates, -
DELETE /users/:id
for user delete.
Each endpoint above already implemented validation as well according to the DTO, for example:
Now let's jump to the user service. In the service, we'll create functions to interact with the database via Prisma and they'll represent each endpoint mentioned above.
Here are the users.service
boilerplate:
import { ConflictException, HttpException, Injectable } from '@nestjs/common';
import { User } from '@prisma/client';
import { PrismaService } from 'src/core/services/prisma.service';
import { CreateUserDto } from './dtos/create-user.dto';
import { hash } from 'bcrypt';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
// async registerUser
// async loginUser
// async updateUser
// async deleteUser
}
Let's start with the register function first:
async registerUser(createUserDto: CreateUserDto): Promise<User> {
try {
// create new user using prisma client
const newUser = await this.prisma.user.create({
data: {
email: createUserDto.email,
password: await hash(createUserDto.password, 10), // hash user's password
name: createUserDto.name,
},
});
// remove password from response
delete newUser.password;
return newUser;
} catch (error) {
// check if email already registered and throw error
if (error.code === 'P2002') {
throw new ConflictException('Email already registered');
}
// throw error if any
throw new HttpException(error, 500);
}
}
registerUser
will be responsible for handling register logic. We can see that now we interact with database using prisma this.prisma.user.create
. We also encrypt the password to make our API secure hash(createUserDto.password, 10)
and remove it from the response API delete newUser.password
.
We also handle the error using throw new ConflictException
if there is a duplicate email or any other error throw new HttpException
.
Now let's move on to login function:
Login response will return an object with a single property called access_token
(interface LoginResponse
) which contains some user data (interface UserPaylod
) generated by using jwt
, so we need to install @nestjs/jwt
first:
$ npm install --save @nestjs/jwt
Once @nestjs/jwt
is installed, update UsersService's constructor
by adding private jwtService: JwtService,
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
Last but not least, update our app.module.ts
by adding jwt module as a global:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './modules/users/users.module';
import { CoreModule } from './core/core.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
UsersModule,
CoreModule,
// add jwt module
JwtModule.register({
global: true,
secret: 'super_secret_key',
signOptions: { expiresIn: '12h' },
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Please take note that the
secret
andexpiresIn
should be stored in a safe place like.env
file. We'll cover this in the next section.
Once jwt
configuration is completed, let's create a new interface file called src/modules/users/interfaces/users-login.interface.ts
:
export interface UserPayload {
sub: number;
name: string;
email: string;
}
export interface LoginResponse {
access_token: string;
}
Here is the loginUser
code:
async loginUser(loginUserDto: LoginUserDto): Promise<LoginResponse> {
try {
// find user by email
const user = await this.prisma.user.findUnique({
where: { email: loginUserDto.email },
});
// check if user exists
if (!user) {
throw new NotFoundException('User not found');
}
// check if password is correct by comparing it with the hashed password in the database
if (!(await compare(loginUserDto.password, user.password))) {
throw new UnauthorizedException('Invalid credentials');
}
const payload: UserPayload = {
// create payload for JWT
sub: user.id, // sub is short for subject. It is the user id
email: user.email,
name: user.name,
};
return {
// return access token
access_token: await this.jwtService.signAsync(payload),
};
} catch (error) {
// throw error if any
throw new HttpException(error, 500);
}
}
So far we already created two functions, registerUser
and loginUser
. Now let's continue on updateUser
and deleteUser
.
Here is the updateUser
function:
async updateUser(id: number, updateUserDto: UpdateUsertDto): Promise<User> {
try {
// find user by id. If not found, throw error
await this.prisma.user.findUniqueOrThrow({
where: { id },
});
// update user using prisma client
const updatedUser = await this.prisma.user.update({
where: { id },
data: {
...updateUserDto,
// if password is provided, hash it
...(updateUserDto.password && {
password: await hash(updateUserDto.password, 10),
}),
},
});
// remove password from response
delete updatedUser.password;
return updatedUser;
} catch (error) {
// check if user not found and throw error
if (error.code === 'P2025') {
throw new NotFoundException(`User with id ${id} not found`);
}
// check if email already registered and throw error
if (error.code === 'P2002') {
throw new ConflictException('Email already registered');
}
// throw error if any
throw new HttpException(error, 500);
}
}
and last function deleteUser
:
async deleteUser(id: number): Promise<string> {
try {
// find user by id. If not found, throw error
const user = await this.prisma.user.findUniqueOrThrow({
where: { id },
});
// delete user using prisma client
await this.prisma.user.delete({
where: { id },
});
return `User with id ${user.id} deleted`;
} catch (error) {
// check if user not found and throw error
if (error.code === 'P2025') {
throw new NotFoundException(`User with id ${id} not found`);
}
// throw error if any
throw new HttpException(error, 500);
}
}
Now the UsersService
is completed and here is the final full code:
// src/modules/users/users.service.ts
import {
ConflictException,
HttpException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { User } from '@prisma/client';
import { PrismaService } from 'src/core/services/prisma.service';
import { CreateUserDto } from './dtos/create-user.dto';
import { compare, hash } from 'bcrypt';
import { LoginUserDto } from './dtos/login-user.dto';
import { JwtService } from '@nestjs/jwt';
import { LoginResponse, UserPayload } from './interfaces/users-login.interface';
import { UpdateUsertDto } from './dtos/update-user.dto';
@Injectable()
export class UsersService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async registerUser(createUserDto: CreateUserDto): Promise<User> {
try {
// create new user using prisma client
const newUser = await this.prisma.user.create({
data: {
email: createUserDto.email,
password: await hash(createUserDto.password, 10), // hash user's password
name: createUserDto.name,
},
});
// remove password from response
delete newUser.password;
return newUser;
} catch (error) {
// check if email already registered and throw error
if (error.code === 'P2002') {
throw new ConflictException('Email already registered');
}
// throw error if any
throw new HttpException(error, 500);
}
}
async loginUser(loginUserDto: LoginUserDto): Promise<LoginResponse> {
try {
// find user by email
const user = await this.prisma.user.findUnique({
where: { email: loginUserDto.email },
});
// check if user exists
if (!user) {
throw new NotFoundException('User not found');
}
// check if password is correct by comparing it with the hashed password in the database
if (!(await compare(loginUserDto.password, user.password))) {
throw new UnauthorizedException('Invalid credentials');
}
const payload: UserPayload = {
// create payload for JWT
sub: user.id, // sub is short for subject. It is the user id
email: user.email,
name: user.name,
};
return {
// return access token
access_token: await this.jwtService.signAsync(payload),
};
} catch (error) {
// throw error if any
throw new HttpException(error, 500);
}
}
async updateUser(id: number, updateUserDto: UpdateUsertDto): Promise<User> {
try {
// find user by id. If not found, throw error
await this.prisma.user.findUniqueOrThrow({
where: { id },
});
// update user using prisma client
const updatedUser = await this.prisma.user.update({
where: { id },
data: {
...updateUserDto,
// if password is provided, hash it
...(updateUserDto.password && {
password: await hash(updateUserDto.password, 10),
}),
},
});
// remove password from response
delete updatedUser.password;
return updatedUser;
} catch (error) {
// check if user not found and throw error
if (error.code === 'P2025') {
throw new NotFoundException(`User with id ${id} not found`);
}
// check if email already registered and throw error
if (error.code === 'P2002') {
throw new ConflictException('Email already registered');
}
// throw error if any
throw new HttpException(error, 500);
}
}
async deleteUser(id: number): Promise<string> {
try {
// find user by id. If not found, throw error
const user = await this.prisma.user.findUniqueOrThrow({
where: { id },
});
// delete user using prisma client
await this.prisma.user.delete({
where: { id },
});
return `User with id ${user.id} deleted`;
} catch (error) {
// check if user not found and throw error
if (error.code === 'P2025') {
throw new NotFoundException(`User with id ${id} not found`);
}
// throw error if any
throw new HttpException(error, 500);
}
}
}
The next step is we need to update our UsersController
to interact with usersService
and here is the full code:
// src/modules/users/users.controller.ts
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import { CreateUserDto } from './dtos/create-user.dto';
import { UpdateUsertDto } from './dtos/update-user.dto';
import { LoginUserDto } from './dtos/login-user.dto';
import { UsersService } from './users.service';
import { User } from '@prisma/client';
import { LoginResponse } from './interfaces/users-login.interface';
@Controller('users')
export class UsersController {
// inject users service
constructor(private readonly usersService: UsersService) {}
@Post('register')
async registerUser(@Body() createUserDto: CreateUserDto): Promise<User> {
// call users service method to register new user
return this.usersService.registerUser(createUserDto);
}
@Post('login')
loginUser(@Body() loginUserDto: LoginUserDto): Promise<LoginResponse> {
// call users service method to login user
return this.usersService.loginUser(loginUserDto);
}
@Get('me')
me(): string {
return 'Get my Profile!';
}
@Patch(':id')
async updateUser(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUsertDto,
): Promise<User> {
// call users service method to update user
return this.usersService.updateUser(+id, updateUserDto);
}
@Delete(':id')
async deleteUser(@Param('id', ParseIntPipe) id: number): Promise<string> {
// call users service method to delete user
return this.usersService.deleteUser(+id);
}
}
We haven't updated the
me
endpoint yet. Will do it later
We're almost done. We need to register UsersController
and UsersService
to UsersModule
:
// src/modules/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Finally, we've done with users
register, login, update, and delete. Now we can test these endpoints:
-
POST /users/register
, -
POST /users/login
, -
PATCH /users/:id
, -
DELETE /users/:id
We've been doing great so far :)
The full code of part 1 can be accessed here:
https://github.com/alfism1/nestjs-api/tree/part-one.
Moving on to part 2:
https://dev.to/alfism1/build-complete-rest-api-feature-with-nest-js-using-prisma-and-postgresql-from-scratch-beginner-friendly-part-2-1g25
Top comments (1)