Skip to content

v1.0.0

Latest
Compare
Choose a tag to compare
@ByeongHunKim ByeongHunKim released this 21 Sep 09:41
· 3 commits to development since this release

Release v1.0.0

1. Role Decorator

  • User Type ( ADMIN, USER ) 에 따라 API 호출 권한 부여
  • PublicApi() decorator 도 존재
// roles.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { UserRole } from '@prisma/client'
import { ROLES_KEY } from '@/src/auth/rbac/roles.decorator'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
      ROLES_KEY,
      [context.getHandler(), context.getClass()],
    )

    if (!requiredRoles) {
      return true
    }

    const req = context.switchToHttp().getRequest()
    if (!req.user) {
      throw new UnauthorizedException()
    }

    return requiredRoles.some((role) => role === req?.user?.role)
  }
}


// role.decorator.ts

import { SetMetadata } from '@nestjs/common'
import { UserRole } from '@prisma/client'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles)

// publicApi.decorator.ts

import { SetMetadata } from '@nestjs/common'

export const IS_PUBLIC_KEY = 'isPublic'
export const PublicApi = () => SetMetadata(IS_PUBLIC_KEY, true)

2. AuthenticatedUser Decorator

  • 유저 정보를 얻기 편하게 해줌 ( 미들웨어 역할 )
  • as-is :
const user = req.user as User // -> user.id 이렇게 접근했어야 했음
  • to-be :
@AuthenticatedUser() user // 파라미터로 받은 후 바로 -> user.id로 사용
import { createParamDecorator, ExecutionContext } from '@nestjs/common'

export const AuthenticatedUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest()
    return request.user
  },
)

3. GlobalPipe

  • pagination에 사용하는 offset, limit 값이 쿼리로 전달될 때 기본값이 string인데, 넘버타입으로 변환해주고, 기본값도 할당해줄 수 있음
import { BadRequestException, PipeTransform, Injectable } from '@nestjs/common'

@Injectable()
export class NumberWithDefaultPipe implements PipeTransform<string, number> {
  constructor(private readonly defaultValue: number) {}

  transform(value: string): number {
    if (!value) {
      return this.defaultValue
    }
    const val = Number(value)
    if (isNaN(val)) {
      throw new BadRequestException(
        `Validation failed. "${val}" is not an integer.`,
      )
    }
    return val
  }
}


/ / 사용

@Query('offset', new NumberWithDefaultPipe(0)) offset,
@Query('limit', new NumberWithDefaultPipe(6)) limit,

4. AppExceptionFilter

  • 원래는 service layer에서 에러만 반환하고, controller layer에서 try/catch문 으로 instanceof Error 종류에 따라서 @nestjs/common에서 제공하는 built-in-exception을 사용했는데, 표준을 custom으로 만들어서 사용하여 error messageerror 코드가 일관성을 가지게 됨
import { Response } from 'express'
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  UnauthorizedException,
  HttpException,
} from '@nestjs/common'
import ApplicationError from '@/src/error/ApplicationError'
import { CustomLogger } from '@/src/logger.service'

const logger = new CustomLogger('AppExceptionFilter')

@Catch(HttpException, ApplicationError)
export class AppExceptionFilter implements ExceptionFilter {
  catch(error: HttpException | ApplicationError, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const request = ctx.getRequest<Request>()
    const response = ctx.getResponse<Response>()
    const status =
      error instanceof HttpException
        ? error.getStatus()
        : error.httpStatusCode ?? 500

    const errorCode = (error as ApplicationError).errorCode ?? ''

    logger.error(
      `${status} ${request.method} ${request.url} : ${JSON.stringify(error)}\n${
        error.stack
      }`,
    )

    // when auth error, hide message for security
    const message =
      error instanceof UnauthorizedException ? 'Unauthorized' : error.message

    response.status(status).json({
      errorCode,
      message,
    })
  }
}
  • custom error
import { CustomError } from 'ts-custom-error'

export interface ApplicationErrorParams {
  message: string
  errorCode: string
  httpStatusCode?: number
}

export default class ApplicationError extends CustomError {
  errorCode: string
  httpStatusCode?: number

  public constructor({
    message,
    errorCode,
    httpStatusCode,
  }: ApplicationErrorParams) {
    super(message)

    this.errorCode = errorCode
    this.httpStatusCode = httpStatusCode
  }
}

5. CustomLogger

  • customLogger를 남겨서 서버 실행 시 e.g dev instance에 접속해서 로그를 확인할 수 있음
  • 정확히 어떤 코드 위치에 무슨 에러가 발생되었는 지 알 수 있어서 디버깅에 용이함
import { ConsoleLogger, Injectable, LogLevel } from '@nestjs/common'

@Injectable()
export class CustomLogger extends ConsoleLogger {
  getTimestamp(): string {
    return new Date().toISOString()
  }

  protected colorize(message: string, logLevel: LogLevel): string {
    if (logLevel !== 'error') {
      return super.colorize(message, logLevel)
    }
    const lines = message.split('\n')
    lines[0] = super.colorize(lines[0], logLevel)
    return lines.join('\n')
  }
}

6. Mapper

  • 기존에는 service layer에서 mapper function을 따로 만들어서 사용했는데 dto가 변경되면 일일이 코드를 바꿔줘야하는 불편함이 있었는데 mapper를 사용하면 exclude에 넣어주고 직접 다른 모듈에서 데이터를 받던지 다른 방법을 통해 넣어줄 수 있어서 훨씬 생산성이 높아짐
export interface MapOption {
  excludes?: string | string[]
}

export class CommonMapper {
  protected mapSourceToTarget<S = object, T = object>(
    source: S,
    target: T,
    option?: MapOption,
  ): T {
    let checker
    if (option?.excludes instanceof Array) {
      checker = option?.excludes?.includes.bind(this)
    } else if (typeof option?.excludes === 'string') {
      checker = (key) => option?.excludes === key
    } else {
      checker = () => false
    }
    Object.getOwnPropertyNames(target).forEach((key) => {
      if (checker(key)) {
        return
      }
      target[key] = source[key]
    })
    return target
  }
}