NestJS 공식문서 번역 겸 공부하는 글 입니다.
의역 및 오역이 있을 수 있습니다.
https://docs.nestjs.com/middleware

Middleware

Middelware

middleware는 route handler 이전에 호출되는 함수이다. middleware는 request와 response object에 접근할 수 있으며, 애플리케이션의 request-response cycle 내에서 next() 함수에도 접근할 수 있다.

middleware

Nest middleware는 기본적으로 express middleware와 같다. 아래는 express 공식문서에 나와있는 express middleware의 특징이다.

  • 어떠한 코드도 실행한다.
  • request와 response object를 변형한다.
  • request-response cycle을 끝낸다.
  • stack에서 다음 middleware를 호출한다.
  • 현재 middleware가 request-reponse cycle을 끝내지 못하면, next() 함수를 통해 다음 middleware를 호출한다.

함수나 @Injectable() 데코레이터를 사용한 class로 Nest custom middleware를 구현할 수 있다. 함수는 특별한 요구사항이 없지만, class는 NestMiddleware interface를 implements 해야한다.

// 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('Request...');
    next();
  }
}

Dependency injection

Nest middleware는 dependency injection을 지원한다. controller나 provider와 마찬가지로 생성자를 통해 의존성을 주입할 수 있다.

Applying middleware

@Module() 데코레이터 인자에는 middleware를 받는 부분이 없다. 대신, module class에 configure() method를 사용하여 적용한다. middleware를 포함하는 module은 NestModule interface를 implements해야 한다.

// app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

위 예제에서 LoggerMiddleware를 /cats route handler에 적용시켰다. 또한 path와 method를 가진 오브젝트를 forRoutes()의 인자로 받아 http method도 제한 가능히다. 아래의 예제처럼 RequestMethod enum을 사용하는 것을 추천한다.

// app.module.ts

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}

configure() method는 async/await를 사용해 비동기로 만들수 있다.

Route wildcards

wildcard로 별표(*)를 사용한 패턴도 제공한다.

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

'ab*cd' route path는 abcd, ab_cd, abecd등과 match된다. ?, +, *, () 같은 문자도 정규식의 부분으로 route path에 사용가능하다. '-', '.'은 string기반 path에 의해 문자 그대로 번역된다.

fastify는 최신버전의 path-to-regexp 패키지를 사용하는데, 이 패키지는 더이상 *을 와일드카드로 지원하지 않는다. 대신, parameter를 사용한다(e.g, (.*), :splat*).

Middleware consumer

middleware consumer는 helper class이다. 이것은 middleware를 관리하는 method들을 제공하며, fluent style chain으로 구성되어 있다. forRoutes() method는 단일 string, 여러개의 string, RouteInfo 오브젝트, 하나의 controller, 여러개의 controller 클래스를 인자로 받을 수 있다. 대부분의 경우 controller의 목록을 콤마(,)로 구분한다. 아래는 단일 controller의 예제이다.

// app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller.ts';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes(CatsController);
  }
}

apply() method는 단일 middleware 또는 여러개의 middleware를 인자로 받을 수 있다. (multiple middleware)

Excluding routes

특정 route만 middleware 적용에서 제외하고 싶은 경우가 있다. exclude() method를 사용해 특정 route를 제외할 수 있다. 해당 method는 단일 string, 여러개의 string, RouteInfo 오브젝트를 인자로 받을 수 있다.

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/(.*)',
  )
  .forRoutes(CatsController);

exclude() method는 path-to-regexp 패키지를 적용해, whildcard parameter를 지원한다.

Functional middleware

위에서 본 LoggerMiddleware는 멤버변수, 추가 method, 의존성도 없이 심플하다. 이렇게 심플한 경우는 함수형 middleware로 정의 가능하다. LoggerMiddleware를 함수형으로 변경해보자.

// logger.middleware.ts

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
};

Multiple middleware

위에서 언급했듯이, 여러개의 middleware를 bind하기 위해서는 apply() method에 콤마(,)로 구분하여 리스트로 제공할 수 있다.

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

Global middleware

모든 route에 한번에 middleware를 bind하고 싶을 때에는, INestApplication 인스턴스에서 제공하는 use() method를 통해 등록가능하다.

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

global middleware에서 DI container에 접근하는 것은 불가능히다. app.use()를 사용하는 대신 함수형 middleware를 사용할 수 있다. 또는 class middleware를 AppModule(또는 다른 module)에 .forRoutes('*')을 사용하여 등록할 수 있다.