NestJS, a progressive Node.js framework, has become a popular choice for building efficient, scalable, and enterprise-grade server-side applications. It leverages TypeScript and incorporates elements of object-oriented programming, functional programming, and reactive programming, making it a versatile tool for modern web development. In this guide, powered by Saigon Digital, we’ll walk you through the steps to build a RESTful API using NestJS.
Why Choose NestJS?
Before diving into the steps, let’s briefly explore why NestJS is a solid choice for building RESTful APIs:
- TypeScript Integration: NestJS is built with TypeScript, offering type safety and robust tooling.
- Modular Architecture: It promotes a modular structure, enhancing the maintainability and scalability of your application.
- Dependency Injection: NestJS comes with a powerful Dependency Injection (DI) system, simplifying the management of your application’s dependencies.
Extensive Ecosystem: It has a rich ecosystem with a vast array of modules for database integration, authentication, and more.
Setting Up the Development Environment
Step 1: Install Node.js and NestJS CLI
Ensure you have Node.js installed on your machine. You can download it from Node.js.
Next, install the NestJS CLI globally:
npm install -g @nestjs/cli
Step 2: Create a New NestJS Project
Create a new NestJS project using the CLI:
nest new my-rest-api
cd my-rest-api
Step 3: Generate a Module
Modules in NestJS are used to organise your application.
Let’s generate a module for our API:
nest generate module users
Step 4: Create a Controller
Controllers handle incoming requests and return responses to the client.
Generate a controller for your users module:
nest generate controller users
Step 5: Define Routes in the Controller
Open the users.controller.ts file and define the RESTful routes:
import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll() {
// Logic to get all users
return 'This action returns all users';
}
@Get(':id')
findOne(@Param('id') id: string) {
// Logic to get a user by ID
return `This action returns user with ID: ${id}`;
}
@Post()
create(@Body() createUserDto: any) {
// Logic to create a new user
return 'This action adds a new user';
}
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: any) {
// Logic to update a user
return `This action updates user with ID: ${id}`;
}
@Delete(':id')
remove(@Param('id') id: string) {
// Logic to delete a user
return `This action removes user with ID: ${id}`;
}
}
Step 6: Create a Service
Services contain the business logic of your application.
Generate a service for the users module:
nest generate service users
Implement the business logic in users.service.ts:
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private users = [];
findAll() {
return this.users;
}
findOne(id: string) {
return this.users.find(user => user.id === id);
}
create(user: any) {
this.users.push(user);
}
update(id: string, updatedUser: any) {
const userIndex = this.users.findIndex(user => user.id === id);
this.users[userIndex] = updatedUser;
}
remove(id: string) {
this.users = this.users.filter(user => user.id !== id);
}
}
Step 7: Inject the Service into the Controller
Modify the users.controller.ts to use the service:
import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@Post()
create(@Body() createUserDto: any) {
this.usersService.create(createUserDto);
return 'User created successfully';
}
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: any) {
this.usersService.update(id, updateUserDto);
return 'User updated successfully';
}
@Delete(':id')
remove(@Param('id') id: string) {
this.usersService.remove(id);
return 'User removed successfully';
}
}
About Saigon Digital
Saigon Digital is a leading digital agency specialising in web development, SEO, UI/UX design, and digital marketing. With a focus on delivering high-quality, innovative solutions, Saigon Digital empowers businesses to thrive in the digital age. Whether you’re a startup or an established enterprise, Saigon Digital has the expertise to help you succeed. For more information, visit Saigon Digital.
NestJS implementation at Saigon Digital Case Studies
At Saigon Digital, we successfully launch our first project using NestJS as RESTful API name Visa Hotel Now and BVIS
Here what we did to leverage Visa Hotel Now implementing NestJS as Server Application
RESTful API Setup
Authentication and Authorisation
In NestJS, guards are a powerful feature used to control the flow of request processing, particularly useful for implementing authentication and authorization. Guards determine whether a request should be processed by the route handler or denied based on custom logic.
Create AdminAuthGuard class to check authentication
import {
ExecutionContext,
Injectable,
UnauthorizedException
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from 'src/decorator/public.decorator';
import { Role } from 'src/model/role.enum';
import { AuthService, REFRESH_TOKEN_COOKIES_KEY } from '../auth.service';
@Injectable()
export class AdminAuthGuard extends AuthGuard('jwt') {
constructor(
private reflector: Reflector,
private authService: AuthService,
) {
super();
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest() as Request;
const userCookie = request.cookies?.[REFRESH_TOKEN_COOKIES_KEY];
if (!userCookie) {
throw new UnauthorizedException('Please login');
}
const role = this.reflector.getAllAndOverride<Role>('role', [
context.getHandler(),
context.getClass(),
]);
if (role === Role.User) {
return true;
}
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any, context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
this.authService.handleAuthErrors(err, user, request);
return user;
}
}
Call AdminAuthGuard inside NestJS uses UseGuards Decorator function. UseGuards is a function in NestJS, it can be imported fromnestjs/common
@ApiTags('Hotel')
@Controller('hotels')
export class HotelsController {
constructor(private readonly hotelsService: HotelService) {}
@HasRoles(Role.Admin)
@UseGuards(AdminAuthGuard)
@Post()
async create(@Body() createHotelDto: CreateHotelDto, @Res() res: Response) {
const result = await this.hotelsService.create(createHotelDto);
if (result) {
return res.status(HttpStatus.OK).json(result);
}
return res.status(HttpStatus.BAD_REQUEST).send();
}
}
Get current user with custom decorator
When using UseGuards, we can get current user and any RESTful API using custom decorator
Create a paramDecorator callback
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
Any RESTful API implementing AuthGuards we can import this User decorator by using it at @User decorator
@UseGuards(AuthGuard('user'))
@Get('account')
@ResponseMessage('Get user information')
async handleGetCurrentAccount(@User() user: IUser) {
return await this.authService.getProfile(user._id);
}
Uploaded File
Uploading files is a common feature in many web applications, and NestJS provides a robust way to handle file uploads. This functionality is typically implemented using the @nestjs/common package, which integrates with Multer, a middleware for handling multipart/form-data primarily used for uploading files.
Create a controller to handle file upload requests. Use the @UseInterceptors decorator along with the FileInterceptor provided by @nestjs/platform-express.
Create a S3 service to handling upload file to AWS S3 bucket
@Controller('images')
export class ImagesController {
constructor(
private readonly s3Service: S3Service,
private readonly imageService: ImageService,
) {}
@Get('folder/:name')
async findByFolder(
@Param('name') name: string,
@Query('limit') pageSize = 36,
@Query('page') pageIndex = 0,
@Query('groupType') groupType: 'AVATAR' | 'ICON' | 'MEDIA' = 'MEDIA',
) {
if (name === 'images') {
return await this.imageService.getAllImage(
pageIndex,
pageSize,
groupType,
);
}
return await this.s3Service.findByFolder(name);
}
@UseGuards(AuthGuard('jwt'))
@Post()
@UseInterceptors(FileInterceptor('files'))
async upload(
@User() user: IUser,
@UploadedFile() files: Express.Multer.File,
@Body() body: { folder: string; groupType: 'AVATAR' | 'ICON' | 'MEDIA' },
) {
const groupType = body.groupType;
const url = await this.s3Service.uploadFile(files, body.folder);
return await this.imageService.create(url, groupType, user);
}
@UseGuards(AuthGuard('jwt'))
@Delete('remove/:id')
async removeImage(
@User() user: IUser,
@Param('id') id: string,
@Body() body: { isPermanent: boolean; image: ImageDocument },
) {
if (body.isPermanent) {
return await this.s3Service.deleteImage(body.image.src);
}
return await this.imageService.delete(id, user);
}
}
Create a images service to handling additional logic. Then we can use these service in upload controller.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Image, ImageDocument } from './image.schema';
import { JwtDecode as IUser } from 'src/model/jwt-decode.model';
type IGroupType = 'AVATAR' | 'ICON' | 'MEDIA';
@Injectable()
export class ImageService {
constructor(
@InjectModel(Image.name) private ImageModel: Model<ImageDocument>,
) {}
async getAllImage(
pageIndex: number,
pageSize: number,
groupType: IGroupType,
) {
const data = await this.ImageModel.find({
groupType,
})
.sort({ createdAt: 'desc' })
.skip(pageIndex * pageSize)
.limit(pageSize)
.exec();
const total = await this.ImageModel.countDocuments({
groupType,
});
return {
data,
total,
};
}
async delete(id: string, user: IUser) {
return await this.ImageModel.findByIdAndDelete(id);
}
async create(src: string, groupType: IGroupType, user: IUser) {
const image = new this.ImageModel({ src, groupType, user: user._id });
await image.save();
return image;
}
}
The Future of Saigon Digital with NestJS
As the digital landscape continues to evolve, Saigon Digital remains at the forefront of innovation by leveraging cutting-edge technologies to deliver exceptional solutions for its clients. One such technology that is shaping the future of Saigon Digital is NestJS, a progressive Node.js framework that is revolutionizing the way we build scalable and efficient server-side applications.
Conclusion
By following these steps, you’ve set up a basic RESTful API using NestJS. This guide covers the essentials, but NestJS offers many more features and capabilities to explore. You can integrate with databases, add authentication, handle validation, and much more to create a robust, production-ready API.
NestJS’s modular architecture and TypeScript integration make it an excellent choice for building scalable and maintainable server-side applications. Start experimenting with more features and dive deeper into the official NestJS documentation to enhance your application further
Ready to elevate your project’s API capabilities? Schedule a call with Saigon Digital today and let’s turn your vision into reality.