TypedMiddleware
TypedMiddleware는 ASAPJS 미들웨어 시스템의 핵심입니다. 일반적인 Express 미들웨어가 단순히 요청을 처리하는 함수인 것과 달리, TypedMiddleware는 라우트 옵션을 받아 미들웨어를 생성하는 팩토리 구조를 가집니다.
이 구조를 통해 두 가지 강력한 기능을 제공합니다:
- 런타임: 라우트별 옵션(
auth: true등)에 따라 동적으로 동작하는 미들웨어 생성 - 컴파일타임: 미들웨어가 요구하는 옵션과 제공하는 컨텍스트를 핸들러까지 자동으로 타입 추론
TypedMiddleware 타입 구조
TypedMiddleware는 @asapjs/types에 다음과 같이 정의되어 있습니다.
// @asapjs/types
export type TypedMiddleware<
RouteOptions extends Record<string, any> = {},
Context extends Record<string, any> = {}
> = ((options: RouteOptions) => (req: any, res: any, next: any) => void) & {
readonly __contextType?: Context;
readonly __errors?: any[] | ((options: RouteOptions) => any[]);
};RouteOptions:@Get,@Post등 데코레이터 옵션에 추가될 필드 정의 (예:auth?: boolean)Context: 미들웨어가req객체에 첨부하여 핸들러에 전달할 데이터 타입 (예:{ user: JwtUserPayload })__errors: 이 미들웨어가 throw할 수 있는 에러 목록.defineMiddleware헬퍼가 자동으로 부착합니다.
TypedMiddleware 만들기 — defineMiddleware 헬퍼
커스텀 미들웨어를 만들 때는 defineMiddleware 헬퍼를 사용합니다. TypedMiddleware 타입을 직접 선언하는 것보다 에러 선언, 타입 강제 등의 기능을 통합적으로 제공합니다.
import { defineMiddleware } from '@asapjs/router';
export const myMiddleware = defineMiddleware<
{ myOption?: boolean }, // RouteOptions: 데코레이터에서 사용할 옵션
{ myContext: string } // Context: 핸들러에서 사용할 데이터
>(
({ myOption = false } = {}) =>
(req, res, next) => {
(req as any).myContext = myOption ? 'Option is enabled' : 'Option is disabled';
next();
}
);defineMiddleware(factory, meta?) 함수 시그니처:
| 인자 | 설명 |
|---|---|
factory | (options: RouteOptions) => (req, res, next) => void — 미들웨어 팩토리 함수 |
meta.errors | 이 미들웨어가 throw할 수 있는 에러 (선택). 배열 또는 함수 형태 지원 |
미들웨어에서 에러 선언하기
defineMiddleware의 meta.errors 옵션을 사용하면, 이 미들웨어가 등록된 모든 라우트의 Swagger 문서에 해당 에러들이 자동으로 포함됩니다.
배열 형태 — 항상 포함
import { error } from '@asapjs/error';
const RateLimitExceeded = error(429, 'RATE_LIMIT_EXCEEDED', '요청 한도를 초과했습니다', {});
export const rateLimitMiddleware = defineMiddleware<{}, {}>(
() => (req, res, next) => { /* ... */ },
{
errors: [RateLimitExceeded], // 이 미들웨어가 적용된 모든 라우트에 포함
}
);함수 형태 — RouteOptions에 따라 동적 결정
라우트 옵션에 따라 에러별로 포함 여부를 개별 제어할 수 있습니다.
import { error } from '@asapjs/error';
import { defineMiddleware } from '@asapjs/router';
const AuthErrors = {
NO_TOKEN: error(403, 'NO_TOKEN_PROVIDED', '토큰이 제공되지 않았습니다', {}),
INVALID_SIGNATURE: error(403, 'INVALID_TOKEN_SIGNATURE', '유효하지 않은 토큰 서명입니다', {}),
UNAUTHORIZED: error(401, 'UNAUTHORIZED', '인증이 만료되었거나 유효하지 않습니다', {}),
};
export const jwtMiddleware = defineMiddleware<
{ auth?: boolean },
{ user: JwtUserPayload }
>(
({ auth = false } = {}) =>
(req, res, next) => {
// auth: false이면 검증 스킵
if (!auth) return next();
// JWT 검증 로직 ...
},
{
// 함수 형태: auth:true인 라우트에만 에러를 Swagger에 포함
errors: (options) =>
options.auth !== false
? [AuthErrors.NO_TOKEN, AuthErrors.INVALID_SIGNATURE, AuthErrors.UNAUTHORIZED]
: [],
}
);이렇게 정의한 미들웨어를 전역으로 등록하면:
@Get('/profile', { auth: true })
// → Swagger에 401(UNAUTHORIZED), 403(NO_TOKEN_PROVIDED), 403(INVALID_TOKEN_SIGNATURE) 자동 포함
@Get('/public', { auth: false })
// → 미들웨어가 실행되지 않으므로 Swagger에 에러 없음Config에 등록하기
작성한 미들웨어를 defineRouterConfig에 등록하고, 프레임워크의 전역 타입을 확장(Augmentation)하여 타입 시스템을 연결합니다.
// src/config.ts
import { defineRouterConfig } from '@asapjs/router';
import type { InferMiddlewareRouteOptions } from '@asapjs/router';
import { myMiddleware } from './middleware/myMiddleware';
export const routerConfig = defineRouterConfig({
middleware: [myMiddleware],
});
export default {
router: routerConfig,
// ...
};
declare module '@asapjs/router' {
// 1. ExecuteArgs에 Context 타입 추가
interface GlobalMiddlewareContext {
myContext: string;
}
// 2. IOptions에 RouteOptions 타입 추가
interface GlobalRouteOptions
extends InferMiddlewareRouteOptions<typeof routerConfig.middleware> {}
}defineRouterConfig(): 미들웨어 배열의 튜플 타입을 보존하여InferMiddlewareRouteOptions가 정확히 추론되게 합니다.GlobalMiddlewareContext:ExecuteArgs의 기본 컨텍스트 타입입니다. 이를 확장하면 모든 핸들러에서 해당 필드를 타입 안전하게 사용할 수 있습니다.GlobalRouteOptions:IOptions를 확장합니다.InferMiddlewareRouteOptions를 사용하면 등록된 모든 미들웨어의 옵션 타입이 자동으로 합산됩니다.
컨트롤러에서 사용하기
등록이 완료되면 데코레이터 옵션에서 미들웨어 옵션을 사용할 수 있고, 핸들러 인자에서 컨텍스트를 바로 꺼낼 수 있습니다.
import { ExecuteArgs, Get, RouterController } from '@asapjs/router';
class MyController extends RouterController {
@Get('/example', {
myOption: true, // ← GlobalRouteOptions에 의해 타입 체크됨
})
public handler = async ({ myContext }: ExecuteArgs) => {
// myContext: string ← GlobalMiddlewareContext에 의해 자동 추론됨
return { message: myContext };
};
}여러 미들웨어 조합
여러 개의 TypedMiddleware를 등록하면 각 미들웨어가 요구하는 옵션과 제공하는 컨텍스트가 자동으로 합쳐집니다.
export const routerConfig = defineRouterConfig({
middleware: [authMiddleware, tenantMiddleware],
});
declare module '@asapjs/router' {
interface GlobalMiddlewareContext {
user: JwtUserPayload; // authMiddleware가 제공
tenantId: string; // tenantMiddleware가 제공
}
interface GlobalRouteOptions
extends InferMiddlewareRouteOptions<typeof routerConfig.middleware> {}
// = { auth?: boolean } & { requireTenant?: boolean }
}동작 원리 (런타임)
TypedMiddleware는 요청 시점이 아닌 라우트 등록 시점에 팩토리가 호출됩니다.
- 설정 로드:
config.router.middleware에 등록된 팩토리 목록을 확인합니다. - 라우트 등록: 컨트롤러의
registerRoutes()가 호출될 때, 각 라우트의IOptions를 모든 팩토리에 전달합니다. - 미들웨어 생성: 각 팩토리는 옵션에 맞는 실제 Express 미들웨어를 반환합니다.
- 체인 구성: 반환된 미들웨어들이 Express 라우트 핸들러 앞에 순서대로 삽입됩니다.
이 방식은 매 요청마다 옵션을 체크하는 오버헤드를 없애고, 라우트 등록 시점에 최적화된 미들웨어 체인을 구성하게 해줍니다.