author-mobile

By Nicholas Rowe,

June 26, 2024

Table of content

    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

    As the CEO and Co-Founder of Saigon Digital, I bring a client-first approach to delivering high-level technical solutions that drive exceptional results to our clients across the world.

    I’m interested in...

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