はじめに
Node.jsでAPIサーバーを書くとき、多くの人がまず思い浮かべるのはExpressでしょう。ですがExpressのミドルウェアはコールバックベースのまま拡張されてきた歴史があり、async/awaitで書こうとするとtry/catchとエラーハンドリングのボイラープレートが積み重なっていきます。
Koaは、Expressを作ったのと同じチームが「もう一度、ゼロから設計し直す」というコンセプトで作ったWebフレームワークです。コア自体は非常に小さく保ちつつ、async/awaitをミドルウェアの標準的な書き方として組み込んでいるのが最大の特徴です。この記事では、Koaの特徴からルーティング・エラーハンドリングまでの実践的な使い方を解説します。
Koaとは
Koaは、Expressの開発チームによって作られたNode.js向けの軽量Webフレームワークです。コアには約570行程度のコードしか含まれておらず、ルーティングやテンプレートエンジンといった機能はあえて標準搭載せず、必要なものをミドルウェアとして組み合わせていくスタイルを取っています。
主な特徴
- async/awaitネイティブ対応 - すべてのミドルウェアを
async関数として書け、コールバック地獄やtry/catchの乱立を避けられる - カスケード型ミドルウェア - リクエストを処理する「下り」と、レスポンスを整形する「上り」を1つの関数の中で自然に書ける
- 最小限のコア - ルーティングやボディパースなどはコアに含めず、
koa-routerやkoa-bodyparserのようなミドルウェアとして必要な分だけ追加する ctxオブジェクトによる統一的なAPI - リクエスト・レスポンスをctx.request・ctx.responseにまとめ、ctx.bodyのような短いショートカットも提供する
インストール
npmまたはyarnでインストールできます。Koaの利用にはNode.js v18.0.0以上が必要です。
npm install koa
yarn add koa
基本的な使い方
最もシンプルな形は、次のようにミドルウェアを1つ登録するだけです。
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
ctx.bodyにレスポンスとして返したい値を代入するだけで、ステータスコードやContent-Typeを自動的に設定してくれます。文字列だけでなくオブジェクトを代入すればJSONとして返却され、res.end()のような呼び出しは不要です。
Koaの真価は、ミドルウェアを複数重ねたときのカスケード(往復)処理にあります。
const Koa = require('koa');
const app = new Koa();
// 1つ目のミドルウェア: 処理時間を計測する
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // 次のミドルウェアへ処理を渡す
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// 2つ目のミドルウェア: 実際のレスポンスを返す
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
await next()を呼ぶと処理は次のミドルウェアに移り、そこでの処理が終わってからawait next()の続きが実行されます。この仕組みによって、1つ目のミドルウェアは「リクエストが来た直後」と「レスポンスが確定した直後」の両方に処理を差し込めます。これがKoaが「カスケード型」と呼ばれる理由です。
実践的なユースケース
koa-routerによるルーティング
Koaのコアにはルーティング機能が含まれていないため、パスやHTTPメソッドごとに処理を分けたい場合はkoa-routerを組み合わせるのが定番です。RESTfulなAPIを組むときの基本形になります。
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/users/:id', async ctx => {
const { id } = ctx.params;
ctx.body = { id, name: `user-${id}` };
});
router.post('/users', async ctx => {
ctx.status = 201;
ctx.body = { message: 'created' };
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
ctx.paramsでURLパラメータを取得できるほか、router.allowedMethods()を登録しておくと、定義していないメソッドでアクセスされた際に自動で405や501を返してくれます。
koa-bodyparserでリクエストボディを扱う
JSONやフォームで送られてきたリクエストボディをパースするミドルウェアです。POST・PUTのAPIを作る際にはほぼ必須になります。
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();
app.use(bodyParser());
router.post('/login', async ctx => {
const { username, password } = ctx.request.body;
if (!username || !password) {
ctx.throw(400, 'username and password are required');
}
ctx.body = { message: `welcome, ${username}` };
});
app.use(router.routes());
app.listen(3000);
bodyParser()をapp.useで先に登録しておくことで、以降のミドルウェアではctx.request.bodyから中身を取り出せるようになります。ctx.throw(400, ...)は指定したステータスコードとメッセージでエラーを送出する、Koa独自のショートカットです。
集中エラーハンドリング
Koaでは各ルートにtry/catchを書かなくても、一番外側のミドルウェアでawait next()をtry/catchで包むだけで、内側のすべてのミドルウェアやルートで発生したエラーをまとめて捕捉できます。
const Koa = require('koa');
const app = new Koa();
// 一番最初に登録し、エラーを一箇所で処理する
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = { error: err.message };
ctx.app.emit('error', err, ctx);
}
});
app.use(async ctx => {
if (ctx.path === '/boom') {
ctx.throw(400, 'invalid request');
}
ctx.body = 'ok';
});
// ログ収集サービスなどに送るためのフック
app.on('error', (err, ctx) => {
console.error('server error', err, ctx.url);
});
app.listen(3000);
app.on('error', ...)を使うと、ミドルウェア内で捕捉した後にもログ収集やアラート通知といった副作用を1箇所にまとめられます。個々のルートハンドラは業務ロジックだけに集中でき、ctx.throwを呼ぶだけでエラーレスポンスの形式を気にする必要がなくなります。
まとめ
Koaは、Expressの資産をそのまま引き継ぐのではなく、async/awaitが標準になった今のNode.jsに合わせてゼロから設計されたフレームワークです。コアを小さく保ちつつ、koa-routerやkoa-bodyparserのようなミドルウェアを必要な分だけ足していくスタイルは、フルスタックなフレームワークに比べて構成の見通しが良くなります。Expressに慣れている方も、まずは今回紹介したカスケード型ミドルウェアと集中エラーハンドリングから試してみて、自分のプロジェクトに合うかを確かめてみてください。