요청 처리
ASAPJS의 모든 라우트 핸들러는 단일 ExecuteArgs 인수를 받습니다. Wrapper 유틸리티(@asapjs/router 내부)는 Express의 원시 Request 및 Response 객체에서 관련 필드를 추출하여, 정규화되고 타입이 지정된 형태로 핸들러에 전달합니다.
ExecuteArgs 인터페이스
import { ExecuteArgs } from '@asapjs/router';정의
export interface ExecuteArgs<P = {}, Q = {}, B = {}> {
req: Request;
res: Response;
path?: P | { [key: string]: any };
query: Q & { [key: string]: any };
body: B & { [key: string]: any };
files?: { [key: string]: any };
user?: any;
paging: PaginationQueryType;
}세 가지 제네릭 타입 파라미터는 선택 사항이며, path, query, body의 추론 타입을 각각 좁히는 데 사용할 수 있습니다.
필드 레퍼런스
| 필드 | 타입 | 출처 | 설명 |
|---|---|---|---|
req | Request | Express 요청 객체 | 원시 Express Request. 다른 필드에서 다루지 않는 항목(헤더, 쿠키, IP 등)에 이 필드를 사용합니다. |
res | Response | Express 응답 객체 | 원시 Express Response. 커스텀 응답(예: 파일 스트림)을 직접 전송해야 할 때만 사용합니다. 일반적인 방법은 핸들러에서 값을 반환하는 것입니다. |
path | P | { [key: string]: any } | req.params | Express가 파싱한 URL 경로 파라미터. 키는 라우트 경로 문자열의 :name 세그먼트와 일치합니다. 모든 값은 문자열로 전달되며, 필요한 경우 수동으로 파싱해야 합니다. |
query | Q & { [key: string]: any } | req.query | 파싱된 쿼리 스트링 객체. DTO 레이어에서 변환하지 않는 한 모든 값은 문자열 또는 문자열 배열입니다. page와 limit 키는 별도로 소비되어 paging으로 들어갑니다. |
body | B & { [key: string]: any } | req.body | 파싱된 요청 바디. application/json 요청의 경우 JSON 디코딩된 객체입니다. multipart/form-data 요청의 경우 바이너리 필드는 files를 사용합니다. |
files | { [key: string]: any } | req.files | multipart/form-data 요청의 파일 업로드. 라우트의 middleware 배열에 파일 업로드 미들웨어(예: multer)가 설정된 경우에만 존재합니다. |
user | any | req.user | 유효한 Bearer 토큰이 있을 때 jwtVerification이 설정하는 디코딩된 JWT 페이로드. 토큰이 제공되지 않은 라우트에서는 undefined. 인증을 참고하세요. |
paging | PaginationQueryType | req.query.page, req.query.limit | 미리 파싱된 페이지네이션 plain object { page, limit }. 항상 존재하며, 쿼리 파라미터가 없을 때 기본값은 page: 0, limit: 20입니다. |
Wrapper가 ExecuteArgs를 구성하는 방법
Wrapper 함수는 등록된 모든 라우트 핸들러에 적용됩니다. 주요 역할은 다음과 같습니다:
req.query.page(기본값0)와req.query.limit(기본값20)를 읽어 정수로 파싱하고PaginationQueryTypeplain object를 생성합니다.req.user,req.query,req.params,req.body,paging값으로ExecuteArgs객체를 조립합니다.ExecuteArgs를 인수로 핸들러를await합니다.- 핸들러가 truthy 값을 반환하면
res.status(200).json(output)으로 응답합니다. - 던져진 오류를 잡아
errorToResponse()로 위임합니다 (아래 오류 처리 섹션 참고).
// packages/router/src/utils/wrapper.ts — 실제 구현
import type { PaginationQueryType } from '@asapjs/sequelize';
import { errorToResponse } from '@asapjs/error';
export interface ExecuteArgs<P = {}, Q = {}, B = {}> {
req: Request;
res: Response;
path?: P | { [key: string]: any };
query: Q & { [key: string]: any };
body: B & { [key: string]: any };
files?: { [key: string]: any };
user?: any;
paging: PaginationQueryType;
}
export default function Wrapper(cb: (args: ExecuteArgs) => Promise<unknown>) {
return async function _Wrapper(req: Request, res: Response, next: NextFunction) {
try {
const user = req.user;
const { page: pageProp = 0, limit: limitProp = 20 } = req.query;
const page = parseInt(String(pageProp), 10);
const limit = parseInt(String(limitProp), 10);
const paging: PaginationQueryType = { page, limit };
const args: ExecuteArgs = {
req, res, query: req.query, path: req.params,
body: req.body, user, paging,
};
const output = await cb(args);
if (output) {
res.status(200).json(output);
}
} catch (err) {
// 서버 에러(500 또는 status 미지정) → 로깅 + Sentry 캡처
// 모든 에러 → errorToResponse(err, res)로 위임
errorToResponse(err, res);
}
};
}PaginationQueryType과 PaginationQueryDto
Wrapper는 모든 요청에서 req.query.page와 req.query.limit을 파싱하여 PaginationQueryType plain object를 생성하고 ExecuteArgs.paging으로 제공합니다.
import { PaginationQueryType, PaginationQueryDto } from '@asapjs/sequelize';PaginationQueryType
PaginationQueryType은 PaginationQueryDto에서 page와 limit만 추출한 타입 별칭입니다. ExecuteArgs.paging의 실제 타입입니다.
type PaginationQueryType = Pick<PaginationQueryDto, 'page' | 'limit'>;
// 결과: { page: number; limit: number }| 필드 | 타입 | 쿼리 파라미터 | 기본값 | 설명 |
|---|---|---|---|---|
page | number | ?page= | 0 | 0 기반 페이지 인덱스. |
limit | number | ?limit= | 20 | 페이지당 레코드 수. |
PaginationQueryDto
PaginationQueryDto는 @asapjs/sequelize의 DTO 클래스로, Swagger 쿼리 파라미터 문서화에 사용합니다. 데코레이터의 query 옵션에 전달하면 Swagger가 page와 limit를 쿼리 파라미터로 문서화합니다.
핸들러에서 paging 사용하기
@Get('/', {
title: 'List posts',
query: PaginationQueryDto,
response: TypeIs.PAGING(PostInfoDto),
})
async getPosts({ paging }: ExecuteArgs) {
// paging.page — ?page=에서 파싱된 정수
// paging.limit — ?limit=에서 파싱된 정수
return await this.postService.getPosts(paging);
}query 옵션에 PaginationQueryDto를 전달하는 것은 Swagger 문서화를 위한 것입니다. paging 필드는 query 설정 여부와 관계없이 Wrapper가 항상 채워줍니다.
응답 반환하기
응답을 전송하는 일반적인 방법은 핸들러에서 값을 반환하는 것입니다:
async getPost({ path }: ExecuteArgs) {
const postId = parseInt((path as any)?.postId as string, 10);
return await this.postService.getPost(postId); // HTTP 200 JSON으로 변환
}Wrapper는 단순 truthy 검사(if (output))로 반환 값을 확인합니다. 따라서:
- 객체, 배열, 또는 truthy 값을 반환하면 →
HTTP 200 application/json. undefined,null, 또는0을 반환하면 → 자동으로 응답이 전송되지 않으므로,res필드를 통해res.json()또는res.end()를 직접 호출해야 합니다.{ success: true }를 반환하면 → 반환할 의미 있는 바디가 없는 삭제 작업에서 자주 사용하는 패턴입니다.
async deletePost({ path, user }: ExecuteArgs) {
await this.postService.deletePost(postId, user);
return { success: true }; // HTTP 200 { "success": true }
}오류 처리
ASAPJS에는 두 가지 에러 처리 경로가 있습니다:
경로 1: Wrapper 내부 — errorToResponse() (@asapjs/error)
라우트 핸들러에서 던져진 에러는 Wrapper의 try/catch에서 잡혀 @asapjs/error의 errorToResponse()로 위임됩니다. 이 경로는 @asapjs/error의 error() 팩토리로 생성한 HttpError와 legacy HttpException을 구분하여 처리합니다.
| 에러 유형 | 조건 | HTTP 상태 | 바디 |
|---|---|---|---|
HttpError (@asapjs/error) | error() 팩토리로 생성 | err.status | { status, errorCode, message, data } |
HttpException (legacy) | status와 message만 있고 errorCode 없음 | err.status | { status, errorCode: 'LEGACY_HTTP_EXCEPTION', message } |
| 처리되지 않은 오류 | status 속성 없음 | 500 | { status: 500, errorCode: 'INTERNAL_SERVER_ERROR', message } |
권장: 새 코드에서는 @asapjs/error의 error() 팩토리를 사용하세요. errorCode와 data를 포함하는 구조화된 에러 응답을 제공합니다.
import { error } from '@asapjs/error';
import { TypeIs } from '@asapjs/schema';
const PostNotFound = error(404, 'POST_NOT_FOUND', '게시글을 찾을 수 없습니다', {
postId: TypeIs.INT({ comment: '게시글 ID' }),
});
// 핸들러에서 사용:
async getPost({ path }: ExecuteArgs) {
const postId = parseInt((path as any)?.postId, 10);
const post = await this.postService.getPost(postId);
if (!post) {
throw PostNotFound({ postId });
// → HTTP 404 { status: 404, errorCode: 'POST_NOT_FOUND', message: '게시글을 찾을 수 없습니다', data: { postId: 42 } }
}
return post;
}경로 2: Express 전역 에러 핸들러 — errorHandler (@asapjs/router)
Wrapper를 거치지 않는 에러(예: 미들웨어에서 next(error) 호출)는 Express 미들웨어 체인 끝에 등록된 errorHandler가 처리합니다. 이 핸들러는 간단한 2-필드 응답을 반환합니다.
| 에러 유형 | HTTP 상태 | 바디 |
|---|---|---|
HttpException | err.status | { status, message } |
| 일반 에러 | 500 | { status: 500, message } |
import { HttpException } from '@asapjs/router';
// HttpException은 간단한 HTTP 에러에 적합합니다:
throw new HttpException(404, 'Post not found');
// → HTTP 404 { status: 404, message: 'Post not found' }HttpException 생성자 시그니처는 인증을 참고하세요.
requestType과 responseType 헬퍼
이 헬퍼 팩토리들은 배열이나 중첩 DTO 구조를 나타내고자 할 때 body 및 response 옵션에 사용할 DtoOrTypeIs 값을 생성합니다.
import { requestType, responseType } from '@asapjs/router';시그니처
const requestType: (
Dto: Constructor | true,
innerType?: 'body' | 'query',
isArray?: boolean
) => () => { type: 'body' | 'query'; data: any } | undefined | true
const responseType: (
Dto: Constructor | true,
innerType?: 'body' | 'query',
isArray?: boolean
) => () => { type: 'body' | 'query'; data: any } | undefined | true| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
Dto | Constructor | true | — | 래핑할 DTO 클래스. 타입은 존재하지만 스키마가 없음을 나타내려면 true를 전달합니다. |
innerType | 'body' | 'query' | 'body' | DTO가 body 컨텍스트를 설명하는지 query 컨텍스트를 설명하는지 지정합니다. |
isArray | boolean | false | true이면 Swagger용 { type: 'array', items: ... } 구조로 DTO 스키마를 감쌉니다. |
사용 예제
import { Post, requestType, responseType } from '@asapjs/router';
import UserInfoDto from '../dto/UserInfoDto';
@Post('/batch', {
title: 'Batch create users',
body: requestType(CreateUserDto, 'body', true), // body에 CreateUserDto 배열
response: responseType(UserInfoDto, 'body', true), // 응답에 UserInfoDto 배열
})
async batchCreate({ body }: ExecuteArgs) {
return await this.userService.batchCreate(body as CreateUserDto[]);
}페이지네이션 응답에는 requestType/responseType 대신 @asapjs/sequelize의 TypeIs.PAGING(Dto)를 사용하세요. 이 메서드가 Swagger UI에 맞는 올바른 엔벨로프 스키마를 생성합니다.
원시 요청 데이터 접근하기
구조화된 필드로 충분하지 않을 때는 req와 res를 직접 사용합니다:
@Get('/download/:fileId', {
title: 'Download file',
auth: true,
})
async downloadFile({ req, res, path }: ExecuteArgs) {
const fileId = (path as any)?.fileId as string;
const stream = await this.fileService.getStream(fileId);
// 커스텀 헤더 설정 및 파이프 — 값을 반환하지 않음
res.setHeader('Content-Disposition', `attachment; filename="${fileId}"`);
stream.pipe(res);
// undefined를 반환하면 Wrapper가 res.json()을 호출하지 않습니다
}