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 message
와error
코드가 일관성을 가지게 됨
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
}
}