JavaScript
NestJS-Request lifecycle
foxlee
2022. 3. 26. 18:43
Request lifecycle
- Incoming request
- Middleware - Globally bound middleware / Module bound middleware
- Guards - Global guards / Controller guards / Route guards
- Interceptors (pre-controller) - Global interceptors / Controller interceptors / Route interceptors
- Pipes - Global pipes / Controller pipes / Route pipes / Route parameter pipes
- Controller (method handler)
- Service (if exists)
- Intercepter (post-request) - Route interceptor / Controller interceptor / Global interceptor
- Exception filters (route, then controller, then global)
- 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,
});
}
}