JavaScript

NestJS-Request lifecycle

foxlee 2022. 3. 26. 18:43

Request lifecycle

  1. Incoming request
  2. Middleware - Globally bound middleware / Module bound middleware
  3. Guards - Global guards / Controller guards / Route guards
  4. Interceptors (pre-controller) - Global interceptors / Controller interceptors / Route interceptors
  5. Pipes - Global pipes / Controller pipes / Route pipes / Route parameter pipes
  6. Controller (method handler)
  7. Service (if exists)
  8. Intercepter (post-request) - Route interceptor / Controller interceptor / Global interceptor
  9. Exception filters (route, then controller, then global)
  10. Server response

Middleware

  • 라우터 핸들러가 호출되기 전에 실행되며, 요청/응답 객체, next함수에 접근 가능
./middlewares/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction) {
        console.log(req.ip);
        next();
    }
}


./app.module.ts
export class AppModule {
    configure(consumer: MiddlewareConsumer) {
        consumer.apply(LoggerMiddleware).forRoutes('*');
    }
}

Guards 

  • 하나의 책임(목적)
  • 런타임에서 요청을 라우터로 보내줄건지(조건-허가,역할 등에 따라)
  • 보통 인증에 사용됨
  • 익스프레스에서는 보통 미들웨어에서 인증을 처리했었음
  • 미들웨어로 처리하는 것도 나쁘진 않음. 토큰 검증 및 유저 데이터들을 요청에 추가하는 것은 특정 라우터에 연결되는 것이 아니기에..)
  • 하지만 미들웨어는 next() 함수를 호출 한 뒤에 어떤 핸들러가 실행될지 모름
  • Guards는 실행컨텍스트 인스턴스에 접근가능(필터,파이프,인터셉터처럼)
@UseGuards(JwtAuthGuard)
@Get('/profile')
    getProfile(@Request() req) {
        return req.user;
	} // JwtAuthGuard 에서 검증되면 JwtStrategy을 통해 req.user에 넣어줌

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
    canActivate(context: ExecutionContext) {
        return super.canActivate(context);
    }

    handleRequest(err, user, info) {
        // passport - authguard jwt 에 의해 토큰 검증
        // user에서 jwt strategy의 validate에 의해 검증 후 반환되는 유저 데이터임
        if (err || !user) {
            throw err || new UnauthorizedException();
        }
        return user;
    }
}

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor() {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: false,
            secretOrKey: jwtConstants.secret,
        });
    }

    async validate(payload: any) {
        return {
            userId: payload.sub,
            username: payload.username,
            email: payload.email,
            test: 'test',
        };
    }
}

// Auth module에서 JWT모듈을 IMPORT 해야 위와같이 로직이 진행됨
@Module({
    imports: [
        forwardRef(() => UsersModule),
        PassportModule,
        JwtModule.register({
            secret: jwtConstants.secret,
            signOptions: { expiresIn: jwtConstants.expiresIn },
        }),
    ],
    controllers: [AuthController],
    providers: [AuthService, JwtStrategy], ## JwtModule에서 해당 JWTStradtegy를 사용
    exports: [AuthService],
})
export class AuthModule {}

 

Interceptor - 라우터 전/후에 실행

  • 미들웨어 다음에 실행되며 라우터 전(아래에서와 같이 console.log('Before...')(에러 발생하지 않은다면 console.log('After...'))에 실행됨
  • 특정 함수 실행 전후로 로직 추가 
  • 함수에서 반환된 결과 변환
  • 함수에서 thrown 된 에러 변환
  • 특정 조건에 따른 함수 오버라이드(캐시 목적 등.)

 

@UseInterceptors(LoggingInterceptor)
async login(@Body() dto: UserLoginDto): Promise<string> {
        return;
    }

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        console.log('Before...');

        const now = Date.now();
        return next
            .handle()
            .pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
    }
}

Pipes - 요청에 담긴 데이터를 검증/변환을 위함

  • transformation: transform input data to the desired form (e.g., from string to integer)
  • validation: evaluate input data and if valid, simply pass it through unchanged; otherwise, throw an exception when the data is incorrect
@Controller('users')
export class UsersController {
    constructor(private readonly usersService: UsersService) {}

	@Get(':id')
    findOne(@Param('id', ParseIntPipe) id: number) {
        return this.usersService.findOne(+id);
    }
}

Exception Filters - 마지막 단계인 서버 응답의 바로 전

@UseFilters(new HttpExceptionFilter())
@Get()
findAll(
) {
    throw new HttpException('error', 400);
}

// ./filters/http-exception.filter.ts
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        // 컨트롤러의 라우터 로직이 실행되고 에러(throw new HttpException('error', 400))가 throw되면 실행됨
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
        const status = exception.getStatus();

        response.status(status).json({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
        });
    }
}