DEV Community

Alfi Samudro Mulyo
Alfi Samudro Mulyo

Posted on • Updated on

Build Complete REST API Feature with Nest JS (Using Prisma and Postgresql) from Scratch - Beginner-friendly - PART 2

Scope of discussion:

  1. Understanding about Guard in Nest JS
  2. Understanding about Nest JS decorator
  3. How to handle private or public routes

In the first part, we've created some users endpoints:

POST /users/register,
POST /users/login,
GET /users/me,
PATCH /users/:id, and
DELETE /users/:id

but we haven't handled this endpoint GET /users/me yet.

Don't worry, we're going to fix it.

We'll only use access_token obtained from the login response to get the user's data.

First, we must understand a guard in Nest JS. What does guard mean? According to the Nest JS documentation, guard is a class annotated with the @Injectable() decorator, which implements the CanActivate interface

Read more about guard.

And what is the purpose of Guard? Basically Guard is like a layer that is responsible for validating or updating any incoming request before being forwarded to their main process.

So for me endpoint, we're gonna validate the access_token and modify the information. Let's dig into this.

Let's create a new file called auth.guard.ts inside src/common/guards/ folder:

// src/common/guards/auth.guard.ts

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET,
      });
      // 💡 We're assigning the payload to the request object here
      // so that we can access it in our route handlers
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we created a class that implements CanActivate interface. Since we implement CanActivate interface, we need to define canActivate function that will be responsible for validating and updating the incoming request. There is also a function called extractTokenFromHeader for validating the access_token included through the header as a Bearer token.

Moving down, we'll see the jwt verification

const payload = await this.jwtService.verifyAsync(token, {
  secret: process.env.JWT_SECRET,
});
Enter fullscreen mode Exit fullscreen mode

To make this work, we need to add a new variable to our .env file JWT_SECRET="super_secret_key"

We might need to update our app.module.ts as well by updating the jwt secret to use .env file secret: 'super_secret_key',

If all validation is passed, it will modify the incoming request information. In this case, request will receive user data from access_token request['user'] = payload;

After we created auth.guard. The next step is to register auth.guard to our root module (a.k.a app.module.ts). Open up app.module.ts and add the auth.guard to providers array:

providers: [
  AppService,
  {
    provide: APP_GUARD,
    useClass: AuthGuard,
  },
],
Enter fullscreen mode Exit fullscreen mode

Great! Now let's continue to users.controller. Open src/modules/users/users.controller.ts file and move to me() function:

// This is before the changes
@Get('me')
me(): string {
  return 'Get my Profile!';
}
Enter fullscreen mode Exit fullscreen mode

Let's modify that function by adding @Request decorator imported from @nestjs/common:

@Get('me')
me(@Request() req) {
  return req.user;
}
Enter fullscreen mode Exit fullscreen mode

Now we're ready to test GET /users/me endpoint:
Users/me test

You might not be able to access other endpoints (including login) since we registered auth.guard globally. We'll fix this in the section below.

Is that all about GET /users/me endpoint? Nope, we can still make an adjustment. If you take a look at the function, req doesn't have an explicit type @Request() req. Since we're using typescript to write our code, will be better to add explicit type.

Let's do this!
We need to create an interface:
src/modules/users/interfaces/express-request-with-user.interface.ts

import { Request as ExpressRequest } from 'express';
import { UserPayload } from './users-login.interface';

export interface ExpressRequestWithUser extends ExpressRequest {
  user: UserPayload & { iat: number; exp: number };
}
Enter fullscreen mode Exit fullscreen mode

Then we can use new interfaces to our me function

@Get('me')
me(@Request() req: ExpressRequestWithUser): UserPayload {
  return req.user;
}
Enter fullscreen mode Exit fullscreen mode

Finally, we're done with GET /users/me endpoint!


Let's move on to the last part of this section. We already created auth.guard and set it as a global Guard. But now we have a problem. If we access the login or register endpoint, we'll get Unauthorized response

{
  "message": "Unauthorized",
  "statusCode": 401
}
Enter fullscreen mode Exit fullscreen mode

As we will have some public endpoints (No need to pass access_token), this will be troublesome.

How do we fix that?

Now we must provide a mechanism for declaring routes as public. For this, we can create a custom decorator using the SetMetadata decorator factory function.

Create a new file src/common/decorators/public.decorator.ts:

import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Enter fullscreen mode Exit fullscreen mode

Update our users.controller by adding @Public() decorator in register and login endpoints:

...

@Public() // <--- Set register as public route
@Post('register')
async registerUser(@Body() createUserDto: CreateUserDto): Promise<User> {
  // call users service method to register new user
  return this.usersService.registerUser(createUserDto);
}

@Public() // <--- Set login as public route
@Post('login')
loginUser(@Body() loginUserDto: LoginUserDto): Promise<LoginResponse> {
  // call users service method to login user
  return this.usersService.loginUser(loginUserDto);
}

...
Enter fullscreen mode Exit fullscreen mode

Lastly, open again auth.guard.ts and add some modify:

// src/common/guards/auth.guard.ts

export class AuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,

    // 💡 See this line
    private reflector: Reflector,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 💡 See this line
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    // 💡 See this condition
    if (isPublic) {
      return true;
    }

    ...

  }
}
Enter fullscreen mode Exit fullscreen mode

Now we're able to access login and register endpoints :)

The final step of this part.

After everything we've done here, let's finalize our users endpoints. So we want to prevent a user from updating or deleting another user's data, for example: in the access_token, it contains user id equal to 2. We don't want user id 2 able to change the name of user id 1.

To achieve that, let's create a new Guard called is-mine.guard

// src/modules/users/users.controller.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class IsMineGuard implements CanActivate {
  constructor() {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();

    // 💡 We can access the user payload from the request object
    // because we assigned it in the AuthGuard
    return parseInt(request.params.id) === request.user.sub;
  }
}
Enter fullscreen mode Exit fullscreen mode

We won't apply is-mine.guard globally like what we did in auth.guard, but we'll apply to routes manually.

Open up users.controller and add @UseGuards(IsMineGuard) decorator to our updateUser anddeleteUser

@Patch(':id')
  @UseGuards(IsMineGuard) // <--- 💡 Prevent user from updating other user's data
  async updateUser(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUsertDto,
  ): Promise<User> {
    // call users service method to update user
    return this.usersService.updateUser(+id, updateUserDto);
  }

  @Delete(':id')
  @UseGuards(IsMineGuard) // <--- 💡 Prevent user from deleting other user's data
  async deleteUser(@Param('id', ParseIntPipe) id: number): Promise<string> {
    // call users service method to delete user
    return this.usersService.deleteUser(+id);
  }
Enter fullscreen mode Exit fullscreen mode

Forbidden resource


The full code of part 2 can be accessed here: https://github.com/alfism1/nestjs-api/tree/part-two

Moving on to part 3:
https://dev.to/alfism1/build-complete-rest-api-feature-with-nest-js-using-prisma-and-postgresql-from-scratch-beginner-friendly-part-3-3j34

Top comments (0)