[ 살펴보기 ] NestJS - Middleware & Guards
![[ 살펴보기 ] NestJS - Middleware & Guards](https://cdn.hashnode.com/res/hashnode/image/upload/v1726501973125/089755b2-fbfa-4736-8f9a-a37508a2cb22.jpeg)
Middleware를 통해 Client request가 Controller에 의해 처리되기 전에 request에 대한 추가 작업을 할 수 있다. Default로 NestJS는 내부적으로 express를 사용하고 있기에 middleware 역시 express의 middleware와 동일하므로 다음과 같은 일을 수행할 수 있다.
Request, Response 객체를 변경할 수 있다.
Response를 반환하거나 Error를 발생시킴으로서 Request, Response lifecycle을 종료할 수 있다.
여러개의 middleware가 사용되고 있을 때 next() middleware 함수를 통해 다음 middleware에게 제어권을 넘길 수 있다. ( middleware에서 response를 return하거나 next를 통해 다른 middleware에 제어권을 넘기지 않으면 request는 처리되지 않고 계속해서 pending 상태에 놓이므로 주의 )
만약 NestJS를 express나 아닌 fastify와 사용한다면 middleware 적용방법이 달라지므로 fastify와 쓸 경우 적용법은 documentation을 참고하자.
다음은 request를 log하는 간단한 middleware다.
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(' ::: LoggerMiddleware ::: ');
console.log(req);
next();
}
}
위의 예제와 같이 Middleware는 NestMiddleware interface를 구현하는 class로 만들 수 있다. 위에서 정의한 Middleware를 app module에 적용시켜 보자.
...
import { LoggerMiddleware } from 'src/middleware/logger.middleware';
@Module({
imports: [
...
OrdersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('orders');
}
}
Middleware를 module에 적용할 때 해당 Module의 class을 통해 적용하며 middleware가 적용되는 module class는 NestModule interface를 구현해야한다. 위의 코드는 /orders route handler로 들어오는 request에 middleware를 적용하는 예제이다.
만약 다음과 같이 설정하면 /orders route handler에 POST method를 통해 들어오는 request에만 middleware를 적용할 수 있다.
...
import {
...
RequestMethod,
} from '@nestjs/common';
import { LoggerMiddleware } from 'src/middleware/logger.middleware';
@Module({
imports: [
...
OrdersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'orders', method: RequestMethod.POST });
}
}
위와 같이 특정 route handler가 아닌 controller가 가진 모든 route handler를 middlware 적용 대상에 포함하고 싶으면 다음과 같이 controller 자체를 forRoutes에 전달해주면 된다.
...
import { OrdersController } from 'src/orders/orders.controller';
@Module({
imports: [
...
OrdersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(OrdersController);
}
}
만약 적용한 controller에서 특정 route handler를 제외하고 싶으면 exclude를 통해 제외할 수 있다.
...
@Module({
imports: [
...
OrdersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude({ path: 'orders', method: RequestMethod.GET }, 'orders/(.*)')
.forRoutes(OrdersController);
}
}
위와 같이 적용하면 /orders와 /orders/123과 같이 order뒤에 path param이 붙는 request를 제외한 OrdersController의 route handler에 middleware를 적용한다.
만약 여러개의 middleware를 적용한다면 apply method에 middleware를 적용할 순서대로 추가해주면 된다. 만약 cookie 관련 작업을 하는 middleware를 logger middleware 이후에 실행시키고 싶다면 다음과 같이 CookieMiddleware를 생성해서 apply method에 추가해준다.
...
@Module({
imports: [
...
OrdersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware, CookieMiddleware)
.forRoutes(OrdersController);
}
}
만약 등록된 모든 route handler에 middleware를 적용하고 싶다면 두 가지 방법이 있다. 첫 번째 방법은 main.ts에서 다음과 같이 middleware를 적용하는 것이다.
...
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
app.use(loggerMidleware)
await app.listen(4000);
}
bootstrap();
다만 main.ts에서 global middleware를 적용할 시 class middleware는 사용할 수 없다. function으로 선언한 middleware만 적용할 수 있으니 주의하자.
만약 class middleware를 global level로 적용하고 싶다면 다음과 같이 app module을 수정해준다.
...
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware, CookieMiddleware)
.forRoutes('*');
}
}
위와 같이 forRoutes를 *로 설정해주면 모든 route handler에 middleware가 적용된다.
Guards
Client가 특정 api에 요청을 보낼 때 해당 client가 api 요청을 위한 권한이 있는지 검증하는 작업은 Guard를 통해 적용한다. Guard는 아래와 같이 CanActive interface를 구현하는 class다.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
validateRequest(request: any) {
return true;
}
}
Guard class에서 구현하는 canActivate method는 ExecutionContext instance를 parameter로 받는다. 그리고 해당 context instance를 통해 현재 request 정보에 접근할 수 있다. context instance를 통해 현재 request 정보를 얻기 위해서 위의 예제처럼 switchToHttp().getRequest method를 통해 정보를 얻을 수 있다.
위의 예제에서 validateRequest method에서 request를 요청한 client의 권한 검증 logic을 적용하고 canActivate method에서 return하는 boolean 값에 따라서 request를 허용할지 허용하지 않을지가 결정된다. true를 return하면 request를 허용하고 false를 return하면 NestJS는 ForbiddenException을 발생시킨다. 만약 다른 Exception을 발생시키고 싶으면 false 대신 원하는 Exception을 throw해줘야 한다.
이제 위에 정의한 Guard를 controller에 적용해보자.
...
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from 'src/guards/auth.guard';
@UseGuards(AuthGuard)
@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService,
) {}
...
}
Guard를 적용할 때는 UseGuard decorator를 통해 적용한다. 만약 위의 예제와 같이 controller에 guard를 적용하면 해당 controller에 소속된 모든 route handler에 해당 guard가 적용된다.
만약에 global scope로 guard를 적용하고 싶다면 main.ts 파일을 아래와 같이 변경해준다.
import { AuthGuard } from 'src/guards/auth.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new AuthGuard());
await app.listen(4000);
}
bootstrap();
위와 같이 global scope로 guard를 적용하면 application 모든 route handler에 등록한 guard가 적용된다. Guard가 실행되는 시점은 middleware가 실행된 이후 그리고 interceptor가 실행되기 이전 단계다.
만약에 Guard에서 authorization header에 담긴 token을 기준으로 client의 권한을 검증한다면 다음과 같이 Guard에서 token 값을 조회한 후 필요한 검증 로직을 추가할 수 있다.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
validateRequest(request: Request) {
const authHeader = request.headers.authorization;
const token = authHeader ? authHeader.split('Bearer ')[1] : authHeader;
// 필요 검증 로직을 적용
const isAuthorized = true;
if (isAuthorized) {
return true;
}
return false;
}
}
![[ 살펴보기 ] RDB - Relationships](https://cdn.hashnode.com/res/hashnode/image/upload/v1739711556668/48dc9e84-a621-42aa-9c9f-5fc5c436f0ec.jpeg)
![[ 살펴보기 ] MySQL - Data types](https://cdn.hashnode.com/res/hashnode/image/upload/v1739593589113/530f8704-4d27-42c9-a451-bb5c63150b99.jpeg)
![[ 살펴보기 ] TypeORM - Transactions, Migration](https://cdn.hashnode.com/res/hashnode/image/upload/v1739106042581/980b8133-61d4-406a-a026-65be9c28eace.jpeg)
![[ 살펴보기 ] TypeORM - Relations](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666874402/b688bd0b-b6bb-4f43-87d8-c1b46b59f1b7.jpeg)
![[ 살펴보기 ] TypeORM - Basics](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666803591/bef5df17-7dc7-4123-ae55-004d5042df39.jpeg)