13.2 Express + TypeScript — Request/Response 타입과 미들웨어
Express + TypeScript 설치
npm install express
npm install --save-dev @types/express typescript @types/node
Request/Response 제네릭 타입
Express의 Request와 Response는 제네릭 타입을 지원합니다.
// Request<Params, ResBody, ReqBody, ReqQuery>
// Response<ResBody>
import { Request, Response } from 'express'
// 완전한 타입 지정
interface UserParams {
id: string
}
interface CreateUserBody {
name: string
email: string
role?: 'admin' | 'user'
}
interface UserQuery {
page?: string
limit?: string
sort?: 'name' | 'email' | 'createdAt'
}
interface UserResponse {
id: string
name: string
email: string
}
// 타입 안전한 핸들러
app.get<UserParams, UserResponse>(
'/users/:id',
async (req: Request<UserParams>, res: Response<UserResponse>) => {
const { id } = req.params // string (타입 안전)
const user = await getUserById(id)
res.json(user)
}
)
app.post<{}, UserResponse, CreateUserBody, UserQuery>(
'/users',
async (req, res) => {
const { name, email, role } = req.body // 타입 안전
const { page } = req.query // string | undefined
const user = await createUser({ name, email, role })
res.status(201).json(user)
}
)
Request 타입 확장 (모듈 보강)
// src/types/express.d.ts
import 'express'
declare module 'express-serve-static-core' {
interface Request {
user?: AuthUser
requestId: string
startTime: number
logger: Logger
}
}
미들웨어 타입
import { Request, Response, NextFunction } from 'express'
// 일반 미들웨어
const requestLogger = (req: Request, res: Response, next: NextFunction) => {
const start = Date.now()
console.log(`→ ${req.method} ${req.path}`)
res.on('finish', () => {
const duration = Date.now() - start
console.log(`← ${res.statusCode} ${duration}ms`)
})
next()
}
// 에러 미들웨어 (매개변수 4개)
const errorHandler = (
error: Error,
req: Request,
res: Response,
next: NextFunction
) => {
console.error(error)
res.status(500).json({ error: error.message })
}
인증 미들웨어
// middleware/auth.ts
import { Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
interface JwtPayload {
userId: string
role: 'admin' | 'user'
}
export async function authMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
try {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) throw new Error('토큰 없음')
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload
req.user = await getUserById(payload.userId)
next()
} catch {
res.status(401).json({ error: '인증 실패' })
}
}
// 권한 체크 미들웨어 팩토리
export function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: '권한 없음' })
}
next()
}
}
라우터 타입 패턴
// routes/user.routes.ts
import { Router } from 'express'
import { authMiddleware, requireRole } from '../middleware/auth'
import { UserController } from '../controllers/user.controller'
import { validateBody } from '../middleware/validate'
import { CreateUserSchema, UpdateUserSchema } from '../schemas/user.schema'
const router = Router()
const controller = new UserController()
router.get('/', controller.getAll)
router.get('/:id', controller.getById)
router.post('/', authMiddleware, validateBody(CreateUserSchema), controller.create)
router.put('/:id', authMiddleware, validateBody(UpdateUserSchema), controller.update)
router.delete('/:id', authMiddleware, requireRole('admin'), controller.delete)
export { router as userRouter }
타입 안전한 에러 처리
// utils/errors.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code?: string
) {
super(message)
this.name = 'AppError'
}
}
export class ValidationError extends AppError {
constructor(
message: string,
public fieldErrors?: Record<string, string[]>
) {
super(message, 422, 'VALIDATION_ERROR')
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource}을(를) 찾을 수 없습니다.`, 404, 'NOT_FOUND')
}
}
// middleware/error.ts
import { Request, Response, NextFunction } from 'express'
import { AppError } from '../utils/errors'
export function errorHandler(
error: unknown,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof AppError) {
return res.status(error.statusCode).json({
error: error.message,
code: error.code,
})
}
if (error instanceof Error) {
console.error('Unexpected error:', error)
return res.status(500).json({ error: '서버 오류가 발생했습니다.' })
}
res.status(500).json({ error: '알 수 없는 오류' })
}
고수 팁
Zod로 요청 본문 자동 검증
// middleware/validate.ts
import { Request, Response, NextFunction } from 'express'
import { ZodSchema } from 'zod'
export function validateBody<T>(schema: ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body)
if (!result.success) {
return res.status(422).json({
error: '유효성 검사 실패',
details: result.error.flatten().fieldErrors,
})
}
req.body = result.data // 파싱된 데이터로 교체
next()
}
}