Express & Fastify
규칙
- 라우트를 도메인별로 구성:
routes/users.ts, routes/orders.ts — 리소스당 하나의 라우터
- 모든 입력을 Zod로 검증: 처리 전
req.body, req.params, req.query 파싱
- 중앙화된 에러 핸들러: Express는 catch-all 미들웨어, Fastify는
setErrorHandler
- 일관된 에러 형식 반환:
{ error: { code: string, message: string, details?: unknown } }
- TypeScript: 제네릭으로 요청/응답 타입 지정 —
Request<Params, ResBody, ReqBody, Query>
- 미들웨어 순서: CORS, body parser, 인증, 속도 제한, 라우트, 에러 핸들러
- 비동기 라우트 핸들러 사용 — 에러 캐치로 감싸거나
express-async-errors 사용
- Fastify: 플러그인 시스템을 사용하여 라우트와 데코레이터 캡슐화 —
fastify.register()
- Fastify 스키마: JSON Schema 정의 또는
@fastify/type-provider-zod를 사용한 검증 + 직렬화
- 헬스 체크:
GET /health — DB/Redis 연결 상태와 함께 200 반환
- Graceful Shutdown: SIGTERM 수신, 새 연결 중단, 진행 중인 요청 완료
패턴
import { Router } from "express";
import { z } from "zod";
const router = Router();
const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(1) });
router.post("/users", async (req, res, next) => {
try {
const body = CreateUserSchema.parse(req.body);
const user = await createUser(body);
res.status(201).json(user);
} catch (err) { next(err); }
});
import Fastify from "fastify";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "fastify-type-provider-zod";
const app = Fastify().withTypeProvider<ZodTypeProvider>();
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
app.post("/users", { schema: { body: CreateUserSchema } }, async (req) => {
return createUser(req.body);
});
피해야 할 것
- 라우트 핸들러에 비즈니스 로직을 넣지 말 것 — 서비스/도메인 레이어로 추출
- Express에서 비동기 에러 처리를 잊지 말 것 — 처리되지 않은 거절은 프로세스를 중단시킴
- 모든 라우트를 하나의 파일에 임포트하지 말 것 — 모듈화를 위해
router.use() 또는 Fastify 플러그인 사용
- "프론트엔드가 검증하니까" 입력 검증을 건너뛰지 말 것 — 항상 서버 측에서 검증