Why NestJS? ๐ค
NestJS, a powerful and scalable framework, is here to revolutionize the way we build APIs. It leverages TypeScript, ensuring your code is clean, easy to debug, and, most importantly, reliable.
The Power of Modularity ๐ช
NestJS is built around the concept of modularity. This means you can organize your code into separate modules, making it easier to understand, maintain, and scale.
Connect ๐
Repo ๐
TOC
- 1. Basic Understanding and Setup
- 2. Setting Up the Auth Controller
- 3. Setting up DB with docker(Prisma)
- 4. Setting up TypeORM with NestJS (Prisma)
- 5. Understanding DTOs and Class-Validator in NestJS
- 6. Implementing Signup Logic with Argon2 and Prisma
- 7. Implementing Login Logic
- 8.Automate postgres restart & prisma migrations
- 9.NestJs config module
- 10. Passport & JWT module setup
- 11. NestGuard
- 12. Custom Param Decorator
- 13. E2E Testing
- 14. Setting Up Test Database
- 15. Dotenv for Development and Testing
- 16. Database Tear Down
- 17. Auth E2E Testing with Pactum
-
18. User E2E Testing with Pactum
- 18.1 Get User with Bearer Token
- 18.2 Edit User with Bearer Token
- 18.2.1 DTO for Edit User
- 18.2.2 Service for Edit User
- 18.2.3 Controller for Edit User
- 18.2.4 Pactum Test for Edit User > โญ Let's Revise the concepts and implement bookmarks [CRUD]
- 19. Bookmarks Crud with Testing E2E
1. Basic Understanding and Setup
1.1 Let's make a module
Create a Basic Module: Start by creating a new folder called
auth
inside thesrc
folder. In theauth
folder, create a new file calledauth.module.ts
.Define the Module: In
auth.module.ts
, define theAuthModule
class and decorate it with@Module({})
. This decorator tells NestJS that this class is a module.
import { Module } from '@nestjs/common';
@Module({})
export class AuthModule {}
-
Use the Module: You can use this module in your application by adding it to the
imports
array of theAppModule
or any other module where you want to use it.
Before:
import { Module } from '@nestjs/common';
@Module({
imports: [],
})
export class AppModule {}
After:
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [AuthModule], // Add the AuthModule to the imports array
})
export class AppModule {}
-
Create More Modules: You can create more modules using the NestJS CLI. For example, to create a
user
module, you can use the following command in the terminal:
nest g module user
This command will create a new folder called user
and a new file called user.module.ts
inside the user
folder. It will also automatically import the UserModule
into the AppModule
.
-
Create a
bookmarks
Module: Similarly, you can create abookmarks
module using the following command:
nest g module bookmarks
-
Final Structure: After creating the
auth
,user
, andbookmark
modules, yourapp.module.ts
file should look like this:
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UserModule } from './user/user.module';
import { BookmarkModule } from './bookmark/bookmark.module';
@Module({
imports: [AuthModule, UserModule, BookmarkModule],
})
export class AppModule {}
This structure allows you to organize your application into distinct modules, each responsible for a specific feature. This modular approach makes your application easier to understand, develop, and test.
1.2 Let's make a basic controller and service
Create the Controller and Service files: In the
auth
folder, create two files:auth.controller.ts
andauth.service.ts
.Define the Controller: In
auth.controller.ts
, define theAuthController
class and decorate it with@Controller('auth')
. This decorator tells NestJS that this class is a controller and maps any incoming requests with the path/auth
to this controller.
import { Controller } from '@nestjs/common';
@Controller('auth')
export class AuthController {}
-
Define the Service: In
auth.service.ts
, define theAuthService
class and decorate it with@Injectable()
. This decorator will allow NestJS to manage this class as a provider that can be injected into other classes.
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {}
-
Register the Controller and Service in the Module: In the
AuthModule
, importAuthController
andAuthService
and add them to thecontrollers
andproviders
arrays respectively. This will allow NestJS to recognize them as part of the module.
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
-
Inject the Service into the Controller: In
AuthController
, inject an instance ofAuthService
through the constructor. This allows you to use the service's methods in the controller.
import { Controller } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
}
Note: The above code is a shorthand for manually creating a new instance of AuthService
in the constructor.
-
Use the Service's methods in the Controller: If you have a method in
AuthService
, you can use it inAuthController
. For example, ifAuthService
has atest()
method:
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
test() {}
}
You can call this method in AuthController
like this:
import { Controller } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {
this.authService.test();
}
}
This is a basic pattern in NestJS for separating the handling of HTTP requests (in the controller) from the business logic (in the service). The controller's job is to receive the request and send the response, while the service performs the business logic.
2. Setting Up the Auth Controller
2.1 Creating Basic Endpoints
-
Create Basic Endpoints: Login and Signup are two common endpoints for authentication. Let's create these basic endpoints in the
AuthController
.
import { Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('signup')
signup() {
return 'I am a signup';
}
@Post('login')
login() {
return 'I am a login';
}
}
Now, if you make a POST request to /auth/signup
or /auth/login
, you will receive the corresponding response.
- Data Types and JSON: With NestJS, you don't have to worry about the data type you send or receive. If you return an object, it will be automatically converted to JSON. If you send a JSON object, it will be automatically parsed. For example:
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('signup')
signup() {
return { message: 'I am a signup' }; // This will be converted to JSON
}
@Post('login')
login() {
return { message: 'I am a login' }; // This will be converted to JSON
}
}
-
Using the AuthService: You can use the
AuthService
to handle the logic for thesignup
andlogin
methods. This keeps your controller and service clean and readable.
In auth.service.ts
:
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
signup() {
return 'I am a signup';
}
login() {
return 'I am a login';
}
}
In auth.controller.ts
:
import { Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('signup')
signup() {
return this.authService.signup();
}
@Post('login')
login() {
return this.authService.login();
}
}
In this setup, the AuthController
handles the HTTP requests and responses, while the AuthService
contains the business logic for signup and login. This separation of concerns makes your code more organized and maintainable.
3. Setting up DB with docker(Prisma)
- Create a
docker-compose.yml
file in the root of your project with the following content:
version: '3.8'
services:
dev-db:
image: postgres:13
ports:
- 5434:5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123
POSTGRES_DB: nest
- Then run
docker compose up dev-db -d
to start the database. - Then
docker ps
to check container id - Then
docker logs <container_id>
to check logs of the container
2024-02-18 19:22:56.603 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2024-02-18 19:22:56.607 UTC [27] LOG: database system was shut down at 2024-02-18 19:22:52 UTC
2024-02-18 19:22:56.614 UTC [1] LOG: database system is ready to accept connections
4. Setting up TypeORM with NestJS (Prisma)
- Install the Prisma CLI globally by running the following command in your terminal:
npm install -g prisma
- Also install the Prisma client as a development dependency in your NestJS project:
npm install @prisma/client
- Now run the following command to initialize Prisma in your project:
npx prisma init
It will create postgres by default, you can change it to mysql or sqlite
Open prisma in your project now paste the following on schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
email String
hash String
firstName String?
lastName String?
}
model Bookmarks {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
title String
Description String
link String
}
- Now modify the .env file's
DATABASE_URL
default
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
change to
DATABASE_URL="postgresql://postgres:123@localhost:5434/nest?schema=public"
- Now migrate the database by running the following command:
npx prisma migrate dev --name init
- You can use prisma studio to check the database by running the following command:
npx prisma studio
- In your bash run the following command:
nest g module prisma
- Now create a prisma service by running the following command:
nest g service prisma --no-spec
- Now modify the prisma.service.ts file to look like this:
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient {
constructor() {
super({
datasources: {
db: {
url: 'postgresql://postgres:123@localhost:5434/nest?schema=public',
},
},
});
}
}
- Now modify the prisma.module.ts file to look like this:
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global() //It will make the service available to all modules
@Module({
providers: [PrismaService],
exports: [PrismaService],//you need to export the service to use it in other modules
})
export class PrismaModule {}
5. Understanding DTOs and Class-Validator in NestJS
Data Transfer Objects (DTOs) are used to structure and validate the data that is sent over the network. They ensure that the data adheres to a specific format and meets certain validation criteria. In NestJS, we often use DTOs with the class-validator
package for this purpose.
5.1 Installing Class-Validator
First, install the class-validator
and class-transformer
packages using npm:
npm i class-validator class-transformer
5.2 Creating and Validating DTOs
Create a dto
folder inside the auth
folder. Inside the dto
folder, create a file named auth.dto.ts
. This file will define the data structure and validation rules for the authentication data.
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class AuthDto {
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
password: string;
}
In the above code, @IsEmail()
, @IsNotEmpty()
, and @IsString()
are decorators provided by class-validator
. They specify that the email should be a valid email address, and both the email and password should be non-empty strings.
5.3 Using DTOs in Controllers
You can now use this DTO in your controller. The @Body()
decorator in combination with the DTO type will automatically validate the incoming request data and throw an error if the data is invalid.
import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthDto } from './dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('signup')
signup(@Body() dto: AuthDto) {
return this.authService.signup(dto);
}
@Post('login')
login() {
return this.authService.login();
}
}
5.4 Global Validation Pipe
To enable automatic validation globally, you can add a global pipe in your main.ts file:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
}));
await app.listen(3333);
}
bootstrap();
The whitelist: true
option will strip any properties that don't have any decorators in the DTO.
This way, you can ensure that all incoming request data is validated according to the rules defined in your DTOs.
This helps to maintain data integrity and security in your application.
6. Implementing Signup Logic with Argon2 and Prisma
In this section, we will create a signup logic using NestJS, Prisma, and argon2 for password hashing.
6.1 Installing Argon2
Argon2 is a password hashing algorithm that is considered to be very secure. It's a better choice than bcrypt. Install it using npm:
npm i argon2
6.2 Basic Signup Logic
Here is the basic signup logic:
- Get the email and password from the request body
- Hash the password using an algorithm
- Create a new user in the database with the email and hashed password
- Return the user data
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuthDto } from './dto';
import * as argon from 'argon2';
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService) {}
async signup(dto: AuthDto) {
const hash = await argon.hash(dto.password); //hashing the password
const user = await this.prisma.user.create({ //creating the user
data: { //data to be created
email: dto.email, //email field from the dto
hash, //hashed password
},
});
return user;
}
}
This will return the following response:
{
"id": 4,
"createdAt": "2024-02-28T04:38:40.804Z",
"updatedAt": "2024-02-28T04:38:40.804Z",
"email": "subham@gmail.com",
"hash": "$argon2id$......",
"firstName": null,
"lastName": null
}
However, we don't want the hash to be returned to the user. We can modify the signup function to select only the necessary fields:
async signup(dto: AuthDto) {
const hash = await argon.hash(dto.password); //hashing the password
const user = await this.prisma.user.create({ //creating the user
data: { //data to be created
email: dto.email, //email field from the dto
hash, //hashed password
},
select: { //selecting the fields to be returned
id: true,
email: true,
firstName: true,
lastName: true,
},
});
return user;
}
Or, you can directly delete the hash:
async signup(dto: AuthDto) {
const hash = await argon.hash(dto.password);
const user = await this.prisma.user.create({
data: {
email: dto.email,
hash,
},
});
delete user.hash; //directly delete
return user;
}
6.3 Handling Unique Email Validation
If we sign up multiple times with the same email, it will create multiple users with the same email. We need to check if the user already exists or not.
First, modify the schema.prisma file to make the email field unique:
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
email String @unique//making the email field unique
hash String
firstName String?
lastName String?
Bookmarks Bookmarks[]
@@map("users") //mapping the table name
}
model Bookmarks {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
title String
Description String
link String
userId Int
user User @relation(fields: [userId], references: [id]) //relation with the user table
@@map("bookmarks")//mapping the table name
}
Now run npx prisma migrate dev
to migrate the database and enter a name for the migration ex: make email unique
. Now run npx prisma studio
to check the database, and you will see the email field is unique now.
6.4 Handling Errors
If you try to sign up with an email that already exists, you will get an error. This is because we are not handling the error, so let's handle the error:
async signup(dto: AuthDto) {
const hash = await argon.hash(dto.password);
try {
const user = await this.prisma.user.create({
data: {
email: dto.email,
hash,
},
});
delete user.hash;
return user;
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new ForbiddenException('Credentials taken');
}
}
throw error;
}
}
Now you can see the error message in the postman:
{
"statusCode": 403,
"message": "Credentials taken",
"error": "Forbidden"
}
6.5 Creating a Custom Exception Filter
If you want to make it more production-ready, you can create a custom exception filter to handle the error. Make a higher-order function to handle the error:
//async-error-handler.ts
import { ForbiddenException } from '@nestjs/common';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
export const asyncErrorHandler = (asyncFun: (...args: any[]) => Promise<any>) => {
return async (...args: any[]) => {
try {
return await asyncFun(...args);
} catch (error) {
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2002') {
throw new ForbiddenException('Credentials taken');
}
throw error;
}
};
};
Now you can use this and without try-catch block:
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuthDto } from './dto';
import * as argon from 'argon2';
import { asyncErrorHandler } from '../errors/async-error-handler';
@Injectable()
export class AuthService {
signup = asyncErrorHandler(async (dto: AuthDto) => {
const hash = await argon.hash(dto.password);
const user = await this.prisma.user.create({
data: {
email: dto.email,
hash,
},
});
delete user.hash;
return user;
});
constructor(private prisma: PrismaService) {}
login() {
return 'I am a login';
}
}
7. Implementing Login Logic
In this section, we will create a login logic using NestJS, Prisma, and argon2 for password verification. The basic login logic involves the following steps:
- Find the user by email.
- If a user does not exist, throw an exception.
- Compare the password.
- If the password does not match, throw an exception.
- Return the user.
Here is the corresponding code:
signin = asyncErrorHandler(async (dto: AuthDto) => {
const user = await this.prisma.user.findUnique({
where: {
email: dto.email,
},
});
if (!user) {
throw new NotFoundException('User not found');
}
const match = await argon.verify(user.hash, dto.password);
if (!match) {
throw new UnauthorizedException('Invalid credentials');
}
delete user.hash;
return user;
});
8. Automating Postgres Restart & Prisma Migrations
In this section, we will automate the process of restarting Postgres and running Prisma migrations. Follow these steps:
- Delete the
package-lock.json
file and runyarn
to install the dependencies. - Modify the script and add the following lines to the
package.json
:
"prisma:dev:deploy": "prisma migrate deploy",
"db:dev:rm": "docker compose rm dev-db -s -f -v",
"db:dev:up": "docker compose up dev-db -d",
"db:dev:restart": "yarn db:dev:rm && yarn db:dev:up && node -e \"setTimeout(() => console.log('Done waiting'), 1000)\" && yarn prisma:dev:deploy",
The prisma:dev:deploy
command will migrate the database to our dev database. The docker compose rm dev-db -s -f -v
command will remove, stop, and remove the volume of the container. The docker compose up dev-db -d
command will start the container in the background. The db:dev:restart
command will remove the container, start the container, wait for 1 second, and then migrate the database to our dev database.
- Run
yarn db:dev:restart
to restart the database and migrate the database to our dev database. - Run
npx prisma studio
to check the database. - Run
yarn start:dev
to start the server.
9. NestJs config module
9.1 Basic Config Setup
- Install the config module using npm:
yarn add @nestjs/config
- import in app.module.ts
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,//this is same as @Global() decorator
}),
],
})
- Now open the
prisma.service.ts
file and modify it to look like this:
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient {
constructor() {
super({
datasources: {
db: {
url: 'postgresql://postgres:123@localhost:5434/nest?schema=public',
},
},
});
}
}
To simply change
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { ConfigService } from '@nestjs/config';
@Injectable() //inject the config service in the constructor
export class PrismaService extends PrismaClient {
constructor(config: ConfigService) {//inject the config service in the constructor
super({
datasources: {
db: {
url: config.get('DATABASE_URL'),//getting the database url from the .env file
},
},
});
}
}
- make sure in you .env file you have the following:
DATABASE_URL="postgresql://postgres:123@localhost:5434/nest?schema=public"
- Now run
yarn start:dev
to start the server.
9.2 Production Config Setup
- Let's try to make it production ready so add a
config
folder in the root of your project and add aconfig.ts
file in it.
-
Update your
config.ts
file: Yourconfig.ts
file should return an object with the configuration for your application. Here's an example:
// config.ts
export default () => ({
database: {
url: process.env.DATABASE_URL,
},
});
In this example, config.ts
exports a function that returns an object with your database configuration. The DATABASE_URL
is read from the environment variables.
-
Update your
app.module.ts
file: In yourapp.module.ts
file, importConfigModule
from@nestjs/config
, import your configuration fromconfig.ts
, and addConfigModule
to theimports
array in the module metadata. Here's how you can do it:
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import configuration from './config/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
}),
// other modules...
],
// controllers, providers, etc...
})
export class AppModule {}
In the above code, load: [configuration]
tells ConfigModule
to load the configuration from the configuration
function.
10. Passport & JWT module setup
10.1 Basic Setup of Passport & JWT
- Install the passport and jwt module using npm:
yarn add @nestjs/passport passport @nestjs/jwt passport-jwt
yarn add -D @types/passport-jwt
- Now open
auth.module.ts
and modify it to look like this:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [JwtModule.register({})],//import the jwt module
//controllers, providers, etc...
})
export class AuthModule {}
Purpose is to sing the token and verify the token
- Now open
auth.service.ts
and modify it to look like this:
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuthDto } from './dto';
//other...
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
signup = asyncErrorHandler(async (dto: AuthDto) => {
//previous code..
});
signin = asyncErrorHandler(async (dto: AuthDto) => {
//previous code..
});
constructor(
private prisma: PrismaService,
private jwt: JwtService,//dependency injection
) {}
}
so we have injected the jwt service in the constructor, so now we can use it in the service
- We need to return the token when user signs in, and send the token when user signs up
- let's make a function which will take the payload and options and return the token
signToken = asyncErrorHandler(async (userId: number, email: string) => {
const payload = {
sub: userId,//sub is the subject of the token
email,//optional
};
const secret = this.config.get('JWT_SECRET');
//token will expire in 15 minutes
//payload is the data you want to store in the token
const token = this.jwt.sign(payload, {
expiresIn: '15m',
secret: secret,
});
return { access_token: token };//return the token
});
-
this.config.get('JWT_SECRET');
import the config service and get the JWT_SECRET from the .env file - and call this function in the signin function and signup function
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuthDto } from './dto';
import * as argon from 'argon2';
import { asyncErrorHandler } from '../errors/async-error-handler';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
signToken = asyncErrorHandler(async (userId: number, email: string) => {
const payload = {
sub: userId,
email,
};
const secret = this.config.get('JWT_SECRET');
const token = this.jwt.sign(payload, {
expiresIn: '15m',
secret: secret,
});
return { access_token: token };
});
signup = asyncErrorHandler(async (dto: AuthDto) => {
const hash = await argon.hash(dto.password);
const user = await this.prisma.user.create({
data: {
email: dto.email,
hash,
},
});
return this.signToken(user.id, user.email); //return the token
});
signin = asyncErrorHandler(async (dto: AuthDto) => {
const user = await this.prisma.user.findUnique({
where: {
email: dto.email,
},
});
if (!user) {
throw new NotFoundException('User not found');
}
const match = await argon.verify(user.hash, dto.password);
if (!match) {
throw new UnauthorizedException('Invalid credentials');
}
return this.signToken(user.id, user.email); //return the token
});
constructor(
private prisma: PrismaService,
private jwt: JwtService,
private config: ConfigService,//call the ConfigService in the constructor
) {}
}
10.2 Let's setup strategy
- We need to validate the token when a user sends the token in the header, and we need to extract the token from the header and validate it
So create a folder called
strategies
in theauth
folder and create a file calledjwt.strategy.ts
in it and index.ts file in thestrategies
folderYou can copy this code from here:
jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()//injectable means it can be injected in other classes as a dependency
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { //extend means it is a subclass of the Strategy class
constructor(//constructor means the strategy is dependent on the prisma service and the config service
config: ConfigService,
private prisma: PrismaService, //private means it is a private property of the class
) {
super({//super means it is calling the constructor of the parent class which is the Strategy class
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), //it says to extract the token from the header
secretOrKey: config.get('JWT_SECRET'), //get the secret from the .env file
});
}
validate(payload: any) { //validate the token and return the payload
return payload;
}
}
index.ts
export * from './jwt.strategy';
It also a provider, so we need to add it to the provider array in the
auth.module.ts
file
providers: [AuthService, JwtStrategy],//add the JwtStrategy to the providers array
11. NestGuard
11.1 Protecting Routes with Guards
- Now we need to protect the routes with the guards, make a user module and a user controller and a user service and a user entity
user module
@Module({
controllers: [UserController]
})
user controler
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('user')
export class UserController {
@UseGuards(AuthGuard('jwt')) //use the jwt guard
@Get('me')
getMe() {
return 'This is the user me';
}
}
11.2 Providers
auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { JwtStrategy } from './strategy';
@Module({
imports: [JwtModule.register({})],
controllers: [AuthController],
providers: [AuthService, JwtService, JwtStrategy],//add the JwtStrategy to the providers array
})
export class AuthModule {}
11.3 Return User Payload
jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
config: ConfigService,
private prisma: PrismaService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('JWT_SECRET'),
});
}
async validate(payload: { sub: number; email: string }) { //return the payload
const user = await this.prisma.user.findUnique({ //find the user by the id
where: {
id: payload.sub,
},
});
delete user.hash; //delete the hash
return user; //return the user
}
}
11.4 JWT Guard
Make a folder in auth name it guard and here use jwt.guard.ts
and index.ts
and then use it in the user.controller.ts
file
jwt.guard.ts
import { AuthGuard } from '@nestjs/passport';
export class JwtGuard extends AuthGuard('jwt') {
constructor() {
super();
}
}
index.ts
export * from './jwt.guard';
user.controller.ts
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
@Controller('user')
export class UserController {
@UseGuards(AuthGuard('jwt'))
@Get('me')
getMe(@Req() req: Request) {
return req.user;
}
}
// Replace with .....
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { Request } from 'express';
import { JwtGuard } from '../auth/guard';
@Controller('user')
export class UserController {
@UseGuards(JwtGuard) //use the jwt guard
@Get('me')
getMe(@Req() req: Request) {
return req.user;
}
}
12. Custom Param Decorator
12.1 Creating a GetUser Decorator
- the purpose is to get the user from the request object and use it in the controller
Decorators are a powerful feature in TypeScript and JavaScript that allow you to add metadata to classes, methods, and properties. In NestJS, decorators are used to define routes, inject dependencies, and more. You can also create custom decorators to add your own functionality to your application.
First make a folder called decorators
in the auth
folder and then make a file called get-user.decorator.ts
and index.ts
in the decorators
folder
get-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const GetUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request: Express.Request = ctx.switchToHttp().getRequest();
return request.user;
});
index.ts
export * from './get-user.decorator';
user.controller.ts
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { Request } from 'express';
import { JwtGuard } from '../auth/guard';
@Controller('user')
export class UserController {
@UseGuards(JwtGuard)
@Get('me')
getMe(@Req() req: Request) {
return req.user;
}
}
// Replace with .....
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtGuard } from '../auth/guard';
import { GetUser } from '../auth/decorator';
import { User } from '@prisma/client';
@UseGuards(JwtGuard) //place it here so that our entire controller is protected
@Controller('user')
export class UserController {
@Get('me')
getMe(@GetUser() user: User) { //use the GetUser decorator
return user;
}
}
- let's return the user id instead of the entire user object
> Modify the
get-user.decorator.ts
file to look like this:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const GetUser = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => {
const request: Express.Request = ctx.switchToHttp().getRequest();
if (data) { //if data is provided then return the user[data]
return request.user[data];
}
return request.user;
});
If you provide a string, it will return the user[data] else it will return the entire user object
- Now let's say you want the email of the user, then you can use the GetUser decorator like this:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtGuard } from '../auth/guard';
import { GetUser } from '../auth/decorator';
import { User } from '@prisma/client';
@UseGuards(JwtGuard)
@Controller('user')
export class UserController {
@Get('me')
getMe(@GetUser() user: User, @GetUser('email') email: string) { //use the GetUser decorator with the email
console.log({
email,
});
return user;
}
}
12.2 Http Decorator
- You can pass you status code which you want to return in the response
auth.controller.ts
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthDto } from './dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(201) //use the @HttpCode decorator
@Post('signup')
signup(@Body() dto: AuthDto) {
return this.authService.signup(dto);
}
@Post('signin')
signin(@Body() dto: AuthDto) {
return this.authService.signin(dto);
}
}
// Or you can use it like this
@HttpCode(HttpStatus.OK) //use the @HttpCode decorator
13. E2E Testing
13.1 Setting up Pactum JS
- Install the pactum js using yarn:
yarn add -D pactum
Open the
test
folder and clear the app.e2e-spec.ts file and modify it to look like this:Open your script in the
package.json
file and add the following:
"test:e2e": "jest --watch --no-cache --config ./test/jest-e2e.json"
This script allows you to run your end-to-end tests using Jest. The --watch flag means Jest will watch your files for changes and rerun tests when necessary. The --no-cache flag disables caching, ensuring that your tests always run with the latest changes. The --config flag specifies the configuration file for Jest.
// Import necessary modules
import { AppModule } from '../src/app.module';
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
// Declare a test suite named 'App e2e'
describe('App e2e', () => {
// Declare a variable to hold our app instance
let app: INestApplication;
// Before all tests, create and initialize the app
beforeAll(async () => {
// Create a module for testing
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();//compile the module
// Create an instance of the app
app = moduleRef.createNestApplication();
// Apply a global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
// Initialize the app
await app.init();
});
// After all tests, close the app
afterAll(() => {
app.close();
});
// Placeholder for a test case
it.todo('should pass');
});
14. Setting Up Test Database
- Open the docker-compose.yml file and modify it to look like this:
Just replicate the dev-db and change the port
version: '3.8'
services:
dev-db:
image: postgres:13
ports:
- 5434:5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123
POSTGRES_DB: nest
test-db:
image: postgres:13
ports:
- 5435:5432 #this should be different from the dev-db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123
POSTGRES_DB: nest
- Similarly modify the script for test database in the
package.json
file
Replicate the script
"prisma:dev:deploy": "prisma migrate deploy",
"db:dev:rm": "docker compose rm dev-db -s -f -v",
"db:dev:up": "docker compose up dev-db -d",
"db:dev:restart": "yarn db:dev:rm && yarn db:dev:up && node -e \"setTimeout(() => console.log('Done waiting'), 1000)\" && yarn prisma:dev:deploy",
"prisma:test:deploy": "dotenv -e .env.test -- prisma migrate deploy",
"db:test:rm": "docker compose rm test-db -s -f -v",
"db:test:up": "docker compose up test-db -d",
"db:test:restart": "yarn db:test:rm && yarn db:test:up && node -e \"setTimeout(() => console.log('Done waiting'), 1000)\" && yarn prisma:test:deploy",
15. Dotenv for Development and Testing
- Installation
yarn add -D dotenv-cli
Make a file name
.env.test
and add the following:Now modify the
package.json
file to look like this:
"prisma:test:deploy": "prisma migrate deploy",
[//]: # (change it to something like this)
"prisma:test:deploy": "dotenv -e .env.test -- prisma migrate deploy",
just add
dotenv -e .env.test --
before the prisma migrate deploy
It's mean that it will use the .env.test file for the test database and the .env file for the development database
Now modify the .env.test file just copy theDATABASE_URL
from the .env file and change the port(which is declared in the docker-compose.yml file)
DATABASE_URL="postgresql://postgres:123@localhost:5435/nest?schema=public"
- Now we add this
"prebuild": "rimraf dist",
It will remove the dist folder before building the project
- Now change the
test:e2e
script in thepackage.json
file to look like this:
"test:e2e": "jest --watch --no-cache --config ./test/jest-e2e.json"
[//]: # (change it to something like this)
"pretest:e2e": "yarn db:test:restart",
"test:e2e": "dotenv -e .env.test -- jest --watch --no-cache --config ./test/jest-e2e.json"
pretest:e2e": "yarn db:test:restart
, will restart the test database before running the test- Just add
dotenv -e .env.test --
before the jest command so that it will use the .env.test file for the test database- It means that it will use the .env.test file for the test database and the .env file for the development database
- For checking the migration in the test database, run the following command:
yarn test:e2e
docker ps //two container should be running
//for check the prisma studio for the test database
npx dotenv -e .env.test prisma studio
//for check the prisma studio for the dev database
npx prisma studio
16. Database Tear Down
16.1 Cleaning the Database
We need to clean the database after the test, but we don't want to restart docker-compose every time, because it takes time to restart the database
- Open
prisma.service.ts
and modify it to look like this:
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class PrismaService extends PrismaClient {
constructor(config: ConfigService) {
super({
datasources: {
db: {
url: config.get('DATABASE_URL'),
},
},
});
}
cleanDb() { //add the cleanDb function
return this.$transaction([this.bookmark.deleteMany(), this.user.deleteMany()]);
}
}
The cleanDb
function is used to clean up the database. It's often used in testing (like before running end-to-end tests) to ensure a consistent starting state for each test run. Here's what it does:
Deletes all bookmark:
this.bookmark.deleteMany()
deletes all records from thebookmarks
table. This is done first because thebookmarks
table likely has a foreign key constraint on theuser
table. If we tried to delete the users first, the database would throw an error because there would still be bookmarks in the database referencing those users.Deletes all users:
this.user.deleteMany()
deletes all records from theuser
table. This is done after deleting all bookmarks to avoid violating any foreign key constraints.
Both deletion operations are wrapped in a transaction (this.$transaction([])
) to ensure atomicity. This means if one operation fails, the other won't be executed, leaving your database in a consistent state.
In summary, cleanDb
is a utility function that helps maintain a clean, predictable state in your database, which is especially useful when running tests. It first deletes bookmarks and then users to respect the foreign key relationship from bookmarks to users.
16.2 Database Tear Down in E2E Tests
- Open the
app.e2e-spec.ts
file and modify it to look like this:
import { AppModule } from '../src/app.module';
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { PrismaService } from '../src/prisma/prisma.service';
describe('App e2e', () => {
let app: INestApplication;
let prisma: PrismaService;//declare the prisma service
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
await app.init();
prisma = app.get(PrismaService); //get the prisma service from the app
await prisma.cleanDb();//clean the database before running the test
});
afterAll(() => { //if you close the app the datebase will be cleaned up
app.close();
});
it.todo('should pass');
});
16.3 Create the test
- First declare this
describe('Auth', () => {});
describe('User', () => {});
describe('Bookmarks', () => {});
- Now define this properly
describe('Auth', () => {
describe('Signup', () => {
it.todo('should signup');
});
describe('Signin', () => {
it.todo('should signin');
});
});
describe('User', () => {
describe('Get me', () => {});
describe('Edit user', () => {});
});
describe('Bookmarks', () => {
describe('Create bookmarks', () => {});
describe('Get bookmarks', () => {});
describe('Get bookmark by id', () => {});
describe('Edit bookmark', () => {});
describe('Delete bookmark', () => {});
});
- Now run the test
yarn test:e2e
it
is a function used to define a test in Jest.it.todo
is a special version ofit
used when you want to plan a test but aren't ready to write it yet. So,it.todo('should signup')
means you're planning to write a test for the signup functionality, but haven't written the test code yet. When Jest runs, it will remind you that this test still needs to be written.
17. Auth E2E Testing with Pactum
17.1 Signup test
import { AppModule } from '../src/app.module';
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { PrismaService } from '../src/prisma/prisma.service';
import { AuthDto } from '../src/auth/dto';
import * as pactum from 'pactum';
describe('App e2e', () => {
let app: INestApplication;
let prisma: PrismaService;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
await app.init();
await app.listen(3333);
prisma = app.get(PrismaService);
await prisma.cleanDb();
pactum.request.setBaseUrl('http://localhost:3333');//base url set
});
afterAll(() => {
app.close();
});
describe('Auth', () => {
describe('Signup', () => {
it('should signup', () => {//remove the todo and write the test
const dto: AuthDto = { //create a dto object
email: 'subham@gmail.com',
password: 'Subham@123',
};
return pactum.spec().post('/auth/signup').withBody(dto).expectStatus(201); //send the request and expect the status 201
// .inspect(); //if you want to see the response
});
});
describe('Signin', () => {
it.todo('should signin');
});
});
describe('User', () => {
describe('Get me', () => {});
describe('Edit user', () => {});
});
describe('Bookmarks', () => {
describe('Create bookmarks', () => {});
describe('Get bookmarks', () => {});
describe('Get bookmark by id', () => {});
describe('Edit bookmark', () => {});
describe('Delete bookmark', () => {});
});
it.todo('should pass');
});
17.2 Signin test
describe('Auth', () => {
const dto: AuthDto = {//delare here for reusing
email: 'subham@gmail.com',
password: 'Subham@123',
};
describe('Signup', () => {
it('should signup', () => {
return pactum.spec().post('/auth/signup').withBody(dto).expectStatus(201);
// .inspect();
});
});
describe('Signin', () => { //same as the signup
it('should signin', () => {
return pactum.spec().post('/auth/signin').withBody(dto).expectStatus(200);
// .inspect();
});
});
17.3 Error handling
describe('Auth', () => {
const dto: AuthDto = {
email: 'subham@gmail.com',
password: 'Subham@123',
};
describe('Signup', () => {
it('should throw if email empty', () => { //add the expectStatus(400)
return pactum
.spec()
.post('/auth/signup')
.withBody({
password: dto.password,
})
.expectStatus(400);
});
it('should signup', () => {
return pactum.spec().post('/auth/signup').withBody(dto).expectStatus(201);
// .inspect();
});
});
describe('Signin', () => {
it('should throw if no body provided', () => {//add the expectStatus(400)
return pactum.spec().post('/auth/signup').expectStatus(400);
});
it('should signin', () => {
return pactum.spec().post('/auth/signin').withBody(dto).expectStatus(200);
// .inspect();
});
});
});
17.4 Multiple Error handling
describe('Auth', () => {
const dto: AuthDto = {
email: 'subham@gmail.com',
password: 'Subham@123',
};
describe('Signup', () => {
it('should throw if email empty', () => {
return pactum
.spec()
.post('/auth/signup')
.withBody({
password: dto.password,
})
.expectStatus(400);
});
it('should throw if password empty', () => {
return pactum
.spec()
.post('/auth/signup')
.withBody({
email: dto.email,
})
.expectStatus(400);
});
it('should throw if no body provided', () => {
return pactum.spec().post('/auth/signup').expectStatus(400);
});
it('should signup', () => {
return pactum.spec().post('/auth/signup').withBody(dto).expectStatus(201);
// .inspect();
});
});
describe('Signin', () => {
it('should throw if email empty', () => {
return pactum
.spec()
.post('/auth/signin')
.withBody({
password: dto.password,
})
.expectStatus(400);
});
it('should throw if password empty', () => {
return pactum
.spec()
.post('/auth/signin')
.withBody({
email: dto.email,
})
.expectStatus(400);
});
it('should throw if no body provided', () => {
return pactum.spec().post('/auth/signup').expectStatus(400);
});
it('should signin', () => {
return pactum.spec().post('/auth/signin').withBody(dto).expectStatus(200);
// .inspect();
});
});
});
17.5 Storing the token
- We need to check use information for that we need to send bearer token in the header > .stores('userAt', 'access_token'); > - stores is used to store the token in the variable userAt and access_token is the key > - if you inspect the response you will see the key access_token just copy it
describe('Auth', () => {
const dto: AuthDto = {
email: 'subham@gmail.com',
password: 'Subham@123',
};
describe('Signup', () => {
it('should throw if email empty', () => {
return pactum
.spec()
.post('/auth/signup')
.withBody({
password: dto.password,
})
.expectStatus(400);
});
it('should throw if password empty', () => {
return pactum
.spec()
.post('/auth/signup')
.withBody({
email: dto.email,
})
.expectStatus(400);
});
it('should throw if no body provided', () => {
return pactum.spec().post('/auth/signup').expectStatus(400);
});
it('should signup', () => {
return pactum.spec().post('/auth/signup').withBody(dto).expectStatus(201);
// .inspect();
});
});
describe('Signin', () => {
it('should throw if email empty', () => {
return pactum
.spec()
.post('/auth/signin')
.withBody({
password: dto.password,
})
.expectStatus(400);
});
it('should throw if password empty', () => {
return pactum
.spec()
.post('/auth/signin')
.withBody({
email: dto.email,
})
.expectStatus(400);
});
it('should throw if no body provided', () => {
return pactum.spec().post('/auth/signup').expectStatus(400);
});
it('should signin', () => {
return pactum
.spec()
.post('/auth/signin')
.withBody(dto)
.expectStatus(200)
.stores('userAt', 'access_token');//use here
// .inspect();
});
});
});
18. User E2E Testing with Pactum
18.1 Get User with Bearer Token
describe('User', () => {
describe('Get me', () => {
it('should get current user', () => {
return pactum
.spec()
.get('/user/me')
.withHeaders({//send the token in the header
Authorization: 'Bearer $S{userAt}',//herer userAt is the variable name and access_token is the key
})
.expectStatus(200);
});
});
describe('Edit user', () => {});
});
18.2 Edit User with Bearer Token
18.2.1 DTO for Edit User
- First write the controller and service for the edit user
- nest g service user --no-spec
user dto edit
dto use for the validation of the data
import { IsEmail, IsOptional, IsString } from 'class-validator';
export class EditUserDto {
@IsEmail() //the email should be a valid email
@IsOptional()//optional
email?: string;
@IsString()//the first name should be a string
@IsOptional()//optional
firstName?: string;
@IsString()//the last name should be a string
@IsOptional()//optional
lastName?: string;
}
18.2.2 Service for Edit User
user service
service use for the business logic
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { EditUserDto } from './dto';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {} //inject the prisma service
async editUser(userId: number, dto: EditUserDto) { //It will take the user id and the dto
//update the user by the user id
const user = await this.prisma.user.update({
where: {
id: userId,
},
data: {
...dto,
},
});
delete user.hash;//delete the hash
return user;
}
}
18.2.3 Controller for Edit User
user.controller.ts
import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
import { JwtGuard } from '../auth/guard';
import { GetUser } from '../auth/decorator';
import { User } from '@prisma/client';
import { EditUserDto } from './dto';
import { UserService } from './user.service';
@UseGuards(JwtGuard)
@Controller('user')
export class UserController {
constructor(private userService: UserService) {}// UserService from the service
@Get('me')
getMe(@GetUser() user: User) {
return user;
}
@Patch() //use the patch method
editUser(@GetUser('id') userId: number, @Body() dto: EditUserDto) { // use the GetUser decorator to get the user id and the dto
return this.userService.editUser(userId, dto); //call the editUser function from the service
}
}
18.2.4 Pactum Test for Edit User
- Now write the test for the edit user
describe('Edit user', () => {
it('should edit user', () => {
const dto: EditUserDto = {
firstName: 'Subham',
email: 'subham1@gmail.com',
lastName: 'Maity'
};
return pactum
.spec()
.patch('/users')
.withHeaders({
Authorization: 'Bearer $S{userAt}',
})
.withBody(dto)
.expectStatus(200)
.expectBodyContains(dto.firstName)
.expectBodyContains(dto.email);;
});
});
19. Bookmarks Crud with Testing E2E
โ Step 0: Schema Define
- Open prisma folder and modify the schema.prisma file to look like this:
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
hash String
firstName String?
lastName String?
bookmarks Bookmark[]
@@map("users")
}
//Bookmark model
model Bookmark {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
description String?
link String
userId Int
user User @relation(fields: [userId], references: [id])
@@map("bookmarks")
}
โ Step 1: Installation
First, we need to generate the service and controller for bookmarks. Run the following commands:
nest g service bookmark --no-spec
nest g controller bookmark --no-spec
โ Step 2: Controller Structure
Next, we will set up the structure of our controller. Here are the steps:
- Declare the
UseGuards
for authentication. - The constructor should be called inside
BookmarkController
withprivate bookmarksService: BookmarksService
. -
BookmarksService
should haveprivate prisma: PrismaService
inside it. - Declare all the routes in the
bookmark.controller.ts
file.
Here's an example of what your bookmark.controller.ts
file might look like:
import { Controller, Delete, Get, Patch, Post, UseGuards } from '@nestjs/common';
import { JwtGuard } from '../auth/guard';
import { BookmarksService } from './bookmark.service';
@UseGuards(JwtGuard)
@Controller('bookmarks')
export class BookmarkController {
constructor(private bookmarkService: BookmarkService) {}
@Get()
getBookmarks() {}
@Get()
getBookmarkById() {}
@Post()
createBookmark() {}
@Patch()
editBookmarkById() {}
@Delete()
deleteBookmarkById() {}
}
โ Step 3: - Decorators for the Bookmarks
- use
Param decorator
for those routes which need the id of the bookmark
info: we already created a get-user.decorator.ts file in the auth folder, so we can use it here
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const GetUser = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => {
const request: Express.Request = ctx.switchToHttp().getRequest();
if (data) {
return request.user[data];
}
return request.user;
});
export class BookmarkController {
constructor(private bookmarkService: BookmarkService) {}
@Get()
getBookmarks(@GetUser('id') userId: number) { //a.
console.log(userId);
}
@Get(':id')
getBookmarkById(@GetUser('id') userId: number, @Param('id', ParseIntPipe) bookmarkId: number) {//b.
console.log(userId, bookmarkId);
}
@Post()
createBookmark(@GetUser('id') userId: number, @Body() dto: CreateBookmarkDto) {//c.
console.log(userId, dto);
}
@Patch(':id')
editBookmarkById(//d.
@GetUser('id') userId: number,
@Param('id', ParseIntPipe) bookmarkId: number,
@Body() dto: EditBookmarkDto,
) {
console.log(userId, bookmarkId, dto);
}
@HttpCode(HttpStatus.NO_CONTENT)//e.
@Delete(':id')
deleteBookmarkById(@GetUser('id') userId: number, @Param('id', ParseIntPipe) bookmarkId: number) {
console.log(userId, bookmarkId);
}
}
a.
getBookmarks(@GetUser('id') userId: number)
: This route fetches all bookmark for a user. The@GetUser('id')
decorator retrieves the user's ID.b.
getBookmarkById(@GetUser('id') userId: number, @Param('id', ParseIntPipe) bookmarkId: number)
: This route fetches a specific bookmark by its ID for a user. The@GetUser('id')
decorator retrieves the user's ID, and@Param('id', ParseIntPipe)
retrieves and parses the bookmark ID from the URL.c.
createBookmark(@GetUser('id') userId: number, @Body() dto: CreateBookmarkDto)
: This route creates a new bookmark for a user. The@GetUser('id')
decorator retrieves the user's ID, and@Body()
retrieves the request body data with validation applied.d.
editBookmarkById(@GetUser('id') userId: number, @Param('id', ParseIntPipe) bookmarkId: number, @Body() dto: EditBookmarkDto)
: This route edits a specific bookmark by its ID for a user. The@GetUser('id')
decorator retrieves the user's ID,@Param('id', ParseIntPipe)
retrieves and parses the bookmark ID from the URL, and@Body()
retrieves the request body data with validation applied.e.
deleteBookmarkById(@GetUser('id') userId: number, @Param('id', ParseIntPipe) bookmarkId: number)
: This route deletes a specific bookmark by its ID for a user. The@GetUser('id')
decorator retrieves the user's ID, and@Param('id', ParseIntPipe)
retrieves and parses the bookmark ID from the URL. The@HttpCode(HttpStatus.NO_CONTENT)
decorator sets the HTTP status code to 204 (No Content) when the operation is successful.
โ Step 4: - DTO for Bookmarks
- Make a folder named
dto
inside thebookmarks
folder and then make a file calledcreate-bookmark.dto.ts
,edit-bookmark.dto.ts
andindex.ts
inside thedto
folder
create-bookmark.dto.ts
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateBookmarkDto {
@IsString()
@IsNotEmpty()
title: string;
@IsString()
@IsOptional()
description?: string;
@IsString()
@IsNotEmpty()
link: string;
}
edit-bookmark.dto.ts
import { IsOptional, IsString } from 'class-validator';
export class EditBookmarkDto {
@IsString()
@IsOptional()
title?: string;
@IsString()
@IsOptional()
description?: string;
@IsString()
@IsOptional()
link?: string;
}
index.ts
export * from './create-bookmark.dto';
export * from './edit-bookmark.dto';
โ Step 5: - Service Structure for Bookmarks
- Construct the
BookmarksService
withprivate prisma: PrismaService
inside it.
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateBookmarkDto, EditBookmarkDto } from './dto';
@Injectable()
export class BookmarkService {
constructor(private prisma: PrismaService) {}
getBookmarks(userId: number) {
console.log(userId);
}
getBookmarkById(userId: number, bookmarkId: number) {
console.log(userId, bookmarkId);
}
createBookmark(userId: number, dto: CreateBookmarkDto) {
console.log(userId, dto);
}
editBookmarkById(userId: number, bookmarkId: number, dto: EditBookmarkDto) {
console.log(userId, bookmarkId, dto);
}
deleteBookmarkById(userId: number, bookmarkId: number) {
console.log(userId, bookmarkId);
}
}
โ Step 6: - Bookmarks Service in the Controller
- Inject the
BookmarksService
inside theBookmarkController
and call the service methods inside the routes.
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
ParseIntPipe,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { JwtGuard } from '../auth/guard';
import { GetUser } from '../auth/decorator';
import { CreateBookmarkDto, EditBookmarkDto } from './dto';
import { BookmarkService } from './bookmark.service';
@UseGuards(JwtGuard)
@Controller('bookmarks')
export class BookmarkController {
constructor(private bookmarksService: BookmarkService) {}
@Get()
getBookmarks(@GetUser('id') userId: number) {
return this.bookmarksService.getBookmarks(userId);
}
@Get(':id')
getBookmarkById(@GetUser('id') userId: number, @Param('id', ParseIntPipe) bookmarkId: number) {
return this.bookmarksService.getBookmarkById(userId, bookmarkId);
}
@Post()
createBookmark(@GetUser('id') userId: number, @Body() dto: CreateBookmarkDto) {
return this.bookmarksService.createBookmark(userId, dto);
}
@Patch(':id')
editBookmarkById(
@GetUser('id') userId: number,
@Param('id', ParseIntPipe) bookmarkId: number,
@Body() dto: EditBookmarkDto,
) {
return this.bookmarksService.editBookmarkById(userId, bookmarkId, dto);
}
@HttpCode(HttpStatus.NO_CONTENT)
@Delete(':id')
deleteBookmarkById(@GetUser('id') userId: number, @Param('id', ParseIntPipe) bookmarkId: number) {
return this.bookmarksService.deleteBookmarkById(userId, bookmarkId);
}
}
โ Step 7: - E2E Testing With Business Logic (Service)
- For testing
yarn test:e2e
- โจ Get Empty Bookmarks
app.e2e-spec.ts
describe('Get empty bookmarks', () => {
it('should get empty bookmarks', () => {
return pactum
.spec()
.get('/bookmarks')
.withHeaders({ Authorization: 'Bearer $S{userAt}' })
.expectStatus(200)
.expectBody([]); //body should be an array and length should be 0
});
});
bookmark.service.ts
@Injectable()
export class BookmarksService {
constructor(private prisma: PrismaService) {}
getBookmarks(userId: number) {
return this.prisma.bookmark.findMany({
where: {
userId,
},
});
}
}
- โจ Create Bookmarks
app.e2e-spec.ts
describe('Create bookmarks', () => {
const dto: CreateBookmarkDto = {
title: 'NestJS',
link: 'https://nestjs.com/',
};
it('should create bookmark', () => {
return pactum
.spec()
.post('/bookmarks')
.withHeaders({
Authorization: 'Bearer $S{userAt}',
})
.withBody(dto)
.expectStatus(201)
.stores('bookmarkId', 'id')//store the bookmark id in the variable bookmarkId
.expectBodyContains(dto.title)
.expectBodyContains(dto.link)
});
});
bookmark.service.ts
@Injectable()
export class BookmarksService {
constructor(private prisma: PrismaService) {}
createBookmark(userId: number, dto: CreateBookmarkDto) {
return this.prisma.bookmark.create({
data: {
userId,
...dto,
},
});
}
}
- โจ Get Bookmarks
First it expects nothing in the get request, and then it creates a bookmark, and then it expects the length of the array to be 1
app.e2e-spec.ts
describe('Get bookmarks', () => {
it('should get bookmarks', () => {
return pactum
.spec()
.get('/bookmarks')
.withHeaders({
Authorization: 'Bearer $S{userAt}',
})
.expectStatus(200)
.expectJsonLength(1);//body should be an array and length should be 1
});
});
- โจ Get Bookmark by ID
.stores('bookmarkId', 'id')
when we create the bookmark we store the id in the variable bookmarkId, and then we use it here
app.e2e-spec.ts
describe('Get bookmark by id', () => {
it('should get bookmark by id', () => {
return pactum
.spec()
.get('/bookmarks/{id}')
.withPathParams('id', '$S{bookmarkId}')
.withHeaders({
Authorization: 'Bearer $S{userAt}',
})
.expectStatus(200)
.expectBodyContains('$S{bookmarkId}');//expect the body to contain the bookmark id
});
});
bookmark.service.ts
@Injectable()
export class BookmarksService {
constructor(private prisma: PrismaService) {}
getBookmarkById(userId: number, bookmarkId: number) {
return this.prisma.bookmark.findFirst({
where: {
id: bookmarkId,
userId,
},
});
}
}
- โจ Edit Bookmark by ID
app.e2e-spec.ts
describe('Edit bookmark by id', () => {
const dto: EditBookmarkDto = {
title: 'Kubernetes Course - Full Beginners Tutorial (Containerize Your Apps!)',
description:
'Learn how to use Kubernetes in this complete course. Kubernetes makes it possible to containerize applications and simplifies app deployment to production.',
};
it('should edit bookmark', () => {
return pactum
.spec()
.patch('/bookmarks/{id}')
.withPathParams('id', '$S{bookmarkId}')
.withHeaders({
Authorization: 'Bearer $S{userAt}',
})
.withBody(dto)
.expectStatus(200)
.expectBodyContains(dto.title)
.expectBodyContains(dto.description);
});
});
bookmark.service.ts
async editBookmarkById(userId: number, bookmarkId: number, dto: EditBookmarkDto) {
// get the bookmark by id
const bookmark = await this.prisma.bookmark.findUnique({
where: {
id: bookmarkId,
},
});
// check if user owns the bookmark
if (!bookmark || bookmark.userId !== userId)
throw new ForbiddenException('Access to resources denied');
return this.prisma.bookmark.update({
where: {
id: bookmarkId,
},
data: {
...dto,
},
});
}
- โจ Delete Bookmark by ID
First it expects the length of the array to be 1, then it deletes the bookmark, and then it expects the length of the array to be 0
app.e2e-spec.ts
describe('Delete bookmark by id', () => {
it('should delete bookmark', () => {
return pactum
.spec()
.delete('/bookmarks/{id}')
.withPathParams('id', '$S{bookmarkId}')
.withHeaders({
Authorization: 'Bearer $S{userAt}',
})
.expectStatus(204);
});
it('should get empty bookmarks', () => {
return pactum
.spec()
.get('/bookmarks')
.withHeaders({
Authorization: 'Bearer $S{userAt}',
})
.expectStatus(200)
.expectJsonLength(0);
});
});
bookmark.service.ts
async deleteBookmarkById(userId: number, bookmarkId: number) {
const bookmark = await this.prisma.bookmark.findUnique({
where: {
id: bookmarkId,
},
});
// check if user owns the bookmark
if (!bookmark || bookmark.userId !== userId)
throw new ForbiddenException('Access to resources denied');
await this.prisma.bookmark.delete({
where: {
id: bookmarkId,
},
});
}
It will give you this output:
bash
PS > yarn test:e2e
yarn run v1.22.19
$ docker compose rm test-db -s -f -v
[+] Stopping 1/1
โ Container lesson02-test-db-1 Stopped 0.3s
Going to remove lesson02-test-db-1
[+] Removing 1/0
โ Container lesson02-test-db-1 Removed 0.0s
$ docker compose up test-db -d
[+] Running 1/1
โ Container lesson02-test-db-1 Started 0.0s
Done waiting
$ dotenv -e .env.test -- prisma migrate deploy
Environment variables loaded from .env
Prisma schema loaded from prisma\schema.prisma
Datasource "db": PostgreSQL database "nest", schema "public" at "localhost:5435"
1 migration found in prisma/migrations
Applying migration `20240307030332_init`
The following migration(s) have been applied:
migrations/
โโ 20240307030332_init/
โโ migration.sql
All migrations have been successfully applied.
$ dotenv -e .env.test -- jest --watch --no-cache --config ./test/jest-e2e.json
PASS test/app.e2e-spec.ts (5.369 s)
App e2e
Auth
Signup
โ should throw if email empty (56 ms)
โ should throw if password empty (5 ms)
โ should throw if no body provided (4 ms)
โ should signup (80 ms)
Signin
โ should throw if email empty (3 ms)
โ should throw if password empty (2 ms)
โ should throw if no body provided (2 ms)
โ should signin (66 ms)
User
Get me
โ should get current user (8 ms)
Edit user
โ should edit user (8 ms)
Bookmarks
Get empty bookmarks
โ should get bookmarks (6 ms)
Create bookmark
โ should create bookmark (8 ms)
Get bookmarks
โ should get bookmarks (5 ms)
Get bookmark by id
โ should get bookmark by id (5 ms)
Edit bookmark by id
โ should edit bookmark (10 ms)
Delete bookmark by id
โ should delete bookmark (9 ms)
โ should get empty bookmarks (5 ms)
Test Suites: 1 passed, 1 total
Top comments (1)
Great post. Thank you