author-mobile

By Nicholas Rowe,

June 26, 2024

Building RESTful APIs with NestJS: A Step-by-Step Guide

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.

author-avatar
author-avatar

About the Author

Nicholas Rowe

Technical Director at Saigon Digital. I overlook all things techy and nerdy to make sure the delivery of all our projects run smoothly from a technical point of view. When I'm not staring at the computer screen, I can be found playing football for the famous Saigon Raiders.

I’m interested in...

Give us some info about your project and we’ll be in touch

loading