본문으로 건너뛰기
김신건의 로그

[Koa] Router (@koa/router)

· 수정 · 📖 약 1분 · 431자/단어 #koa #router #routing
Koa Router, @koa/router, koa-router, koa router, koa route, Koa 라우팅

정의

@koa/router 는 Koa 의 공식 라우터입니다. Koa 코어는 라우팅을 제공하지 않으므로 별도 설치가 필요합니다. Express 스타일 라우팅 (HTTP method + URL) 을 지원하며, path-to-regexp 로 파라미터 매칭합니다.

설치

npm i @koa/router

기본

const Koa = require('koa');
const Router = require('@koa/router');

const app = new Koa();
const router = new Router();

router.get('/', async (ctx) => {
  ctx.body = 'Home';
});

router.get('/users/:id', async (ctx) => {
  ctx.body = { id: ctx.params.id };
});

router.post('/users', async (ctx) => {
  ctx.body = ctx.request.body;
});

app
  .use(router.routes())
  .use(router.allowedMethods());

app.listen(3000);
  • router.routes(): HTTP 요청을 매칭 후 handler 실행
  • router.allowedMethods(): 405 Method Not Allowed 지원

HTTP Methods

router.get('/path', handler);
router.post('/path', handler);
router.put('/path', handler);
router.patch('/path', handler);
router.delete('/path', handler);
router.head('/path', handler);
router.options('/path', handler);
router.all('/path', handler);       // 모든 method

Path Parameters

router.get('/users/:id', async (ctx) => {
  ctx.params.id;
});

router.get('/users/:userId/posts/:postId', async (ctx) => {
  ctx.params.userId;
  ctx.params.postId;
});

Optional parameter

router.get('/users/:id?', async (ctx) => {
  // /users -> id undefined
  // /users/1 -> id '1'
});

Regex

router.get('/users/:id(\\d+)', async (ctx) => {
  // id 는 숫자만
});

Wildcard

router.get('/files/(.*)', async (ctx) => {
  const path = ctx.params[0];
});

Query Parameters

Koa 자체가 자동 파싱:

router.get('/search', async (ctx) => {
  ctx.query.q;
  ctx.query.page;
});
// GET /search?q=koa&page=2  ->  {q: 'koa', page: '2'}

Router Middleware

Handler 전에 미들웨어 실행:

async function requireAuth(ctx, next) {
  if (!ctx.state.user) ctx.throw(401);
  await next();
}

router.get('/me', requireAuth, async (ctx) => {
  ctx.body = ctx.state.user;
});

// 여러 미들웨어
router.get('/admin', requireAuth, requireAdmin, async (ctx) => {
  ctx.body = 'admin dashboard';
});

Router.use()

Router 전체에 미들웨어:

router.use(async (ctx, next) => {
  console.log(`${ctx.method} ${ctx.path}`);
  await next();
});

router.use('/admin', requireAdmin);   // 특정 prefix 만

router.use(['/admin', '/dashboard'], requireAuth);

Router Prefix

const router = new Router({prefix: '/api/v1'});

router.get('/users', async (ctx) => {
  // 실제 URL: /api/v1/users
});

Nested Router

const rootRouter = new Router();
const usersRouter = new Router({prefix: '/users'});
const postsRouter = new Router({prefix: '/posts'});

usersRouter.get('/', async (ctx) => { ... });
usersRouter.get('/:id', async (ctx) => { ... });

postsRouter.get('/', async (ctx) => { ... });

rootRouter.use(usersRouter.routes(), usersRouter.allowedMethods());
rootRouter.use(postsRouter.routes(), postsRouter.allowedMethods());

app.use(rootRouter.routes()).use(rootRouter.allowedMethods());

Named Route

router.get('user-detail', '/users/:id', async (ctx) => { ... });

// URL 생성
router.url('user-detail', {id: 42});   // '/users/42'
router.url('user-detail', {id: 42}, {query: {tab: 'profile'}});   
// '/users/42?tab=profile'

Template 렌더링, redirect 에 유용.

Route parameters middleware

특정 param 이 있을 때 미리 처리:

router.param('user', async (id, ctx, next) => {
  ctx.state.user = await getUserById(id);
  if (!ctx.state.user) ctx.throw(404);
  await next();
});

router.get('/users/:user', async (ctx) => {
  ctx.body = ctx.state.user;
});

router.get('/users/:user/posts', async (ctx) => {
  ctx.body = await getPostsByUser(ctx.state.user.id);
});

Router 파일 분할

// routes/users.js
const Router = require('@koa/router');
const router = new Router({prefix: '/users'});
const controller = require('../controllers/users');

router.get('/', controller.list);
router.get('/:id', controller.get);
router.post('/', controller.create);
router.patch('/:id', controller.update);
router.delete('/:id', controller.delete);

module.exports = router;
// routes/index.js
const Router = require('@koa/router');
const usersRouter = require('./users');
const postsRouter = require('./posts');

const router = new Router();

router.use(usersRouter.routes(), usersRouter.allowedMethods());
router.use(postsRouter.routes(), postsRouter.allowedMethods());

module.exports = router;
// app.js
const router = require('./routes');
app.use(router.routes()).use(router.allowedMethods());

Controller 패턴

// controllers/users.js
async function list(ctx) {
  ctx.body = await User.findAll();
}

async function get(ctx) {
  const user = await User.findById(ctx.params.id);
  ctx.assert(user, 404, 'Not found');
  ctx.body = user;
}

async function create(ctx) {
  const {email, name} = ctx.request.body;
  ctx.assert(email && name, 400);
  const user = await User.create({email, name});
  ctx.status = 201;
  ctx.body = user;
}

async function update(ctx) {
  const user = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body);
  ctx.body = user;
}

async function del(ctx) {
  await User.findByIdAndDelete(ctx.params.id);
  ctx.status = 204;
}

module.exports = {list, get, create, update, delete: del};

allowedMethods()

정의된 route 의 method 를 관리:

  • Method Not Allowed (405): 정의된 path 인데 wrong method
  • Not Implemented (501): 서버가 지원 안 함
  • OPTIONS: 자동 응답 (Allow 헤더)
app.use(router.routes()).use(router.allowedMethods({
  throw: true,       // exception 던짐 (직접 handle 가능)
  notImplemented: () => new NotImplementedError(),
  methodNotAllowed: () => new MethodNotAllowedError(),
}));

Route 순서

Koa router 는 정의 순서 로 매칭:

router.get('/users/me', ...);        // 구체적 먼저
router.get('/users/:id', ...);

/users/:id 를 먼저 정의하면 /users/me 를 id=“me” 로 잡아버림.

OPTIONS / CORS

const cors = require('@koa/cors');

app.use(cors({
  origin: 'https://app.example.com',
  credentials: true,
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
}));

app.use(router.routes()).use(router.allowedMethods());

Match 정보

router.get('/users/:id', async (ctx) => {
  console.log(ctx.matched);   // 매칭된 route 목록
  console.log(ctx._matchedRouteName);   // named route
});

Redirect

router.redirect('/old', '/new', 301);

Version 라우팅

Router prefix 또는 header 기반:

const v1 = new Router({prefix: '/api/v1'});
const v2 = new Router({prefix: '/api/v2'});

v1.get('/users', v1Controller.list);
v2.get('/users', v2Controller.list);

app.use(v1.routes());
app.use(v2.routes());

함정

WARNING

Route 순서 실수. /users/:id/users/me 보다 먼저 정의하면 /users/me 를 id=“me” 로 매치. 항상 구체적 route 를 먼저.

CAUTION

router.routes() 를 여러 번 호출 하면 동일 route 여러 번 등록. 한 번만.

WARNING

allowedMethods() 없으면 잘못된 method 요청이 404. 405 로 응답하려면 반드시 등록.

IMPORTANT

router.get('name', '/path', handler) vs router.get('/path', handler). Named vs unnamed.

CAUTION

Nested router 시 prefix 이중 적용. Root router prefix + sub router prefix.

관련 위키

이 글의 용어 (9개)
[FastAPI] Routing (Path Operations)fastapi
정의 FastAPI Routing 은 HTTP method + URL 을 Python 함수 (path operation) 에 매핑하는 시스템입니다. Starlette 의 라우팅 …
[Flask] Routing (URL Rules, Converters, url_for)flask
정의 Flask Routing 은 Werkzeug 의 시스템에 기반한 URL 매핑입니다. 데코레이터 ( , , ...) 로 URL 패턴을 뷰 함수에 연결하고, converter …
[Framework] Koa.jsframeworks
정의 Koa.js 는 Express 창시자 TJ Holowaychuk 이 2013년 발표한 Node.js 웹 프레임워크 입니다. Express 의 후계자 성격이며, 미들웨어를 a…
[Koa] Body Parsingframeworks
정의 Koa Body Parsing 은 HTTP 요청 body 를 파싱해 에 사용 가능한 형태로 만드는 미들웨어입니다. Koa 코어는 body parser 를 포함하지 않으므로 …
[Koa] Context (ctx)frameworks
정의 Koa Context ( ) 는 각 요청마다 생성되어 미들웨어와 route handler 에 전달되는 객체입니다. (Node 원본), (Koa 확장), (Koa 확장), (…
[Koa] Error Handlingframeworks
정의 Koa Error Handling 은 미들웨어 chain 에서 발생한 예외를 catch 하여 응답을 정형화하는 패턴입니다. Koa 는 자동 error handler 를 제공…
[Koa] Middleware (Onion Model)frameworks
정의 Koa Middleware 는 요청/응답 사이에 실행되는 async 함수입니다. 각 미들웨어는 (context) 와 (다음 미들웨어를 실행하는 함수) 를 받고, 로 down…
[Koa] Testing (Supertest, Jest, Vitest)frameworks
정의 Koa Testing 은 (HTTP 호출) + / (assertions/runner) 조합이 표준입니다. 을 Supertest 에 전달하면 실제 HTTP 서버 없이 요청/응…
[NestJS] Controllersframeworks
정의 NestJS Controller 는 데코레이터가 붙은 클래스로, HTTP 요청을 라우팅하고 응답을 반환합니다. Method 별로 , , , , 데코레이터를 붙여 URL + …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기