HTTP 요청이 Controller에 닿기 전, 그 사이 어딘가에서 로직을 실행하고 싶을 때 Middleware를 사용한다. Express의 app.use와 동일한 개념이며, NestJS는 이를 클래스 기반으로 구조화해 제공한다. 요청 로깅, 인증 토큰 파싱, CORS 전처리처럼 "모든 요청에 공통으로 적용할 작업"이 주요 사용처다.
Client Request
│
▼
Middleware ← 여기서 실행
│
▼
Guard
│
▼
Interceptor (pre)
│
▼
Controller
│
▼
Interceptor (post)
│
▼
Exception Filter
│
▼
Client Response
Middleware는 파이프라인의 가장 앞단에 위치한다. next를 호출하지 않으면 요청이 다음 단계로 전달되지 않으므로, 반드시 next 호출을 잊지 않아야 한다.
NestMiddleware 인터페이스를 구현하는 클래스를 만든다.
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) {
const { method, originalUrl } = req;
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[${method}] ${originalUrl} — ${res.statusCode} (${duration}ms)`);
});
next();
}
}
NestMiddleware — use(req, res, next) 메서드 하나를 강제하는 인터페이스. @Injectable을 붙여 DI 컨테이너에 등록한다.
Middleware는 @Module 데코레이터가 아닌 configure 메서드로 등록한다. 모듈 클래스가 NestModule 인터페이스를 구현해야 한다.
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
import { UserController } from './user.controller';
@Module({
controllers: [UserController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*'); // 모든 경로에 적용
}
}
forRoutes에는 문자열 경로, Controller 클래스, 또는 경로 + 메서드 객체를 넘길 수 있다.
// 특정 Controller에만 적용
consumer.apply(LoggerMiddleware).forRoutes(UserController);
// 특정 경로와 HTTP 메서드 조합
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'user', method: RequestMethod.GET });
| 구분 | Middleware | Guard | Interceptor |
|---|---|---|---|
| 실행 시점 | Route 매칭 전 | Route 매칭 후, Handler 전 | Handler 전/후 |
| 실행 컨텍스트 | Express req/res | ExecutionContext | ExecutionContext |
| 주요 용도 | 로깅, 파싱, CORS | 인증/인가 | 응답 변환, 캐싱 |
next 방식 | Express NextFunction | boolean 반환 | Observable 체인 |
Guard는 canActivate의 반환값으로 요청 통과 여부를 결정한다. Interceptor는 RxJS Observable을 반환해 응답 스트림을 조작할 수 있다.
Middleware는 NestJS의 DI 시스템을 활용하면서도 Express 미들웨어 생태계와 호환된다. 함수형 Middleware((req, res, next) => void)도 apply에 바로 전달할 수 있어, helmet이나 compression 같은 서드파티 패키지를 별도 래핑 없이 등록할 수 있다.
import helmet from 'helmet';
configure(consumer: MiddlewareConsumer) {
consumer.apply(helmet(), LoggerMiddleware).forRoutes('*');
}