Express 앱이 커질수록 app.get, app.post가 index.ts 한 파일에 쌓이면서 코드가 빠르게 뒤엉킨다. express.Router는 라우트를 독립 모듈로 분리해 각 도메인이 자신의 경로만 책임지도록 만든다. 이 구조가 실무에서 Controller, Service 레이어와 맞물리는 방식을 살펴본다.
express.Router는 미니 Express 앱처럼 동작하는 독립 라우터 인스턴스다. 자체 Middleware 스택과 라우트를 가지며, 메인 앱에 app.use로 마운트된다.
app.ts
└── app.use('/users', userRouter)
└── userRouter
├── GET / → list users
├── GET /:id → get user
└── POST / → create user
마운트(mount) — app.use(path, router)로 서브 라우터를 특정 경로에 연결하는 것. 이후 router 내부 경로는 마운트 경로를 prefix로 상속한다.
// src/routes/users.ts
import { Router } from 'express';
const router = Router();
router.get('/', (req, res) => {
res.json({ users: [] });
});
router.get('/:id', (req, res) => {
res.json({ id: req.params.id });
});
router.post('/', (req, res) => {
res.status(201).json({ created: true });
});
export default router;
// src/app.ts
import express from 'express';
import userRouter from './routes/users';
const app = express();
app.use(express.json());
app.use('/users', userRouter);
export default app;
Router 인스턴스에 router.use로 해당 라우터 전체에만 적용되는 Middleware를 등록할 수 있다. 인증 같은 횡단 관심사를 라우터 단위로 격리하기 좋다.
import { Router, Request, Response, NextFunction } from 'express';
const router = Router();
// 이 라우터의 모든 경로에 적용
router.use((req: Request, res: Response, next: NextFunction) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
router.get('/profile', (req, res) => {
res.json({ profile: {} });
});
export default router;
:id처럼 경로 파라미터가 존재할 때마다 실행할 콜백을 등록한다. 파라미터 유효성 검사나 DB 조회를 중앙화할 때 유용하다.
router.param('id', (req, res, next, id) => {
if (isNaN(Number(id))) {
return res.status(400).json({ error: 'id must be a number' });
}
next();
});
router.get('/:id', (req, res) => {
res.json({ id: req.params.id });
});
router.param — 특정 파라미터가 라우트에 등장할 때마다 선행 실행되는 콜백. 같은 파라미터를 쓰는 여러 라우트에 반복 검증 로직을 넣지 않아도 된다.
도메인별로 라우터를 분리하고 routes/index.ts에서 통합하면 app.ts가 깔끔하게 유지된다.
src/
├── app.ts
├── routes/
│ ├── index.ts ← 모든 라우터를 통합
│ ├── users.ts
│ ├── posts.ts
│ └── auth.ts
└── controllers/
├── users.ts
└── posts.ts
// src/routes/index.ts
import { Router } from 'express';
import userRouter from './users';
import postRouter from './posts';
import authRouter from './auth';
const router = Router();
router.use('/users', userRouter);
router.use('/posts', postRouter);
router.use('/auth', authRouter);
export default router;
// src/app.ts
import express from 'express';
import router from './routes';
const app = express();
app.use(express.json());
app.use('/api', router); // 최종 경로: /api/users, /api/posts ...
export default app;
| 레이어 | 역할 |
|---|---|
| Router | URL 매핑, Middleware 적용 |
| Controller | 요청 파싱, 응답 직렬화 |
| Service | 비즈니스 로직 |
| Repository | DB 접근 |
라우터는 요청을 올바른 Controller로 전달하는 교통 정리 역할만 맡는다. 비즈니스 로직이 라우터 콜백 안에 직접 들어가기 시작하면 분리가 필요하다는 신호다.