Koa の使い方 — Node.js 向け軽量ミドルウェアフレームワーク
一言でいうと
Koa は、Express の開発チームが設計した次世代の Node.js Web フレームワークです。async/await をネイティブに活用したミドルウェアスタック(通称「オニオンモデル」)により、HTTP サーバーの処理を簡潔かつ直感的に記述できます。
どんな時に使う?
- REST API サーバーの構築 — 必要なミドルウェアだけを選んで組み合わせる、軽量な API サーバーを作りたいとき
- BFF(Backend for Frontend)の実装 — フロントエンドとバックエンドの間に薄い中間サーバーを置き、リクエストの加工やプロキシを行いたいとき
- Express からのモダン化移行 — コールバック地獄から脱却し、
async/awaitベースのクリーンなエラーハンドリングを実現したいとき
インストール
注意: Koa v3 は Node.js v18.0.0 以上が必要です。
# npm
npm install koa
# yarn
yarn add koa
# pnpm
pnpm add koa
TypeScript で使う場合は型定義も追加します。
npm install -D @types/koa
基本的な使い方
最もシンプルな Hello World サーバーです。
import Koa from 'koa';
const app = new Koa();
// ロガーミドルウェア
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// レスポンス
app.use(async (ctx) => {
ctx.body = { message: 'Hello Koa' };
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
オニオンモデルの理解
Koa の最大の特徴は、ミドルウェアが「行き(downstream)」と「帰り(upstream)」の2フェーズで処理される点です。
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
console.log('1. リクエスト受信');
await next();
console.log('5. レスポンス送信');
});
app.use(async (ctx, next) => {
console.log('2. 認証チェック');
await next();
console.log('4. レスポンスヘッダー追加');
ctx.set('X-Response-Time', '42ms');
});
app.use(async (ctx) => {
console.log('3. ビジネスロジック実行');
ctx.body = 'Done';
});
app.listen(3000);
// 実行順序: 1 → 2 → 3 → 4 → 5
よく使う API
1. ctx.body — レスポンスボディの設定
代入する値の型に応じて Content-Type が自動設定されます。
import Koa from 'koa';
import { createReadStream } from 'fs';
const app = new Koa();
app.use(async (ctx) => {
const { path } = ctx;
if (path === '/json') {
// オブジェクト → application/json
ctx.body = { id: 1, name: 'Koa' };
} else if (path === '/html') {
// HTML 文字列 → text/html
ctx.body = '<h1>Hello</h1>';
} else if (path === '/text') {
// プレーン文字列 → text/plain
ctx.body = 'plain text';
} else if (path === '/stream') {
// Stream → application/octet-stream(typeを指定すれば上書き可)
ctx.type = 'xml';
ctx.body = createReadStream('data.xml');
} else {
ctx.status = 404;
ctx.body = { error: 'Not Found' };
}
});
app.listen(3000);
2. ctx.request — リクエスト情報の取得
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx) => {
// よく使うプロパティ(ctx からのショートカットも利用可能)
const info = {
method: ctx.method, // ctx.request.method のショートカット
url: ctx.url, // ctx.request.url のショートカット
path: ctx.path, // クエリ文字列を除いたパス
query: ctx.query, // パース済みクエリオブジェクト
headers: ctx.headers, // リクエストヘッダー
ip: ctx.ip, // クライアント IP
host: ctx.host, // Host ヘッダー
protocol: ctx.protocol, // http or https
accepts: ctx.accepts('json'), // コンテントネゴシエーション
};
ctx.body = info;
});
app.listen(3000);
3. ctx.throw() / ctx.assert() — エラーハンドリング
import Koa from 'koa';
const app = new Koa();
// グローバルエラーハンドラー
app.use(async (ctx, next) => {
try {
await next();
} catch (err: any) {
ctx.status = err.status || 500;
ctx.body = {
error: err.message,
status: ctx.status,
};
// アプリケーションレベルのエラーイベントも発火させる
ctx.app.emit('error', err, ctx);
}
});
app.use(async (ctx) => {
const id = ctx.query.id;
// assert: 条件を満たさない場合にエラーをスロー
ctx.assert(id, 400, 'id パラメータは必須です');
// throw: 明示的にHTTPエラーをスロー
if (id === '999') {
ctx.throw(404, 'ユーザーが見つかりません');
}
ctx.body = { id };
});
// アプリケーションレベルのエラーリスナー
app.on('error', (err, ctx) => {
console.error('Server error:', err.message);
});
app.listen(3000);
4. app.use() — ミドルウェアの登録
import Koa, { Middleware } from 'koa';
const app = new Koa();
// 型付きミドルウェア関数の定義
const cors: Middleware = async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (ctx.method === 'OPTIONS') {
ctx.status = 204;
return;
}
await next();
};
// ミドルウェアファクトリパターン
function rateLimit(maxRequests: number, windowMs: number): Middleware {
const requests = new Map<string, { count: number; resetTime: number }>();
return async (ctx, next) => {
const key = ctx.ip;
const now = Date.now();
const record = requests.get(key);
if (record && now < record.resetTime) {
if (record.count >= maxRequests) {
ctx.throw(429, 'Too Many Requests');
}
record.count++;
} else {
requests.set(key, { count: 1, resetTime: now + windowMs });
}
await next();
};
}
// 登録順序がそのまま実行順序になる
app.use(cors);
app.use(rateLimit(100, 60 * 1000));
app.use(async (ctx) => {
ctx.body = 'OK';
});
app.listen(3000);
5. app.context / ctx.state — コンテキストの拡張と状態管理
import Koa from 'koa';
const app = new Koa();
// app.context にプロパティを追加すると、全リクエストの ctx で利用可能
// ※ v3 でも利用可能ですが、TypeScript では型拡張が必要
declare module 'koa' {
interface DefaultContext {
db: { query: (sql: string) => Promise<any[]> };
}
interface DefaultState {
user: { id: number; name: string } | null;
}
}
// プロトタイプへの追加(全リクエストで共有される)
app.context.db = {
query: async (sql: string) => {
// 実際にはデータベース接続を使う
return [{ id: 1, name: 'example' }];
},
};
// 認証ミドルウェア — ctx.state でリクエスト固有のデータを受け渡す
app.use(async (ctx, next) => {
const token = ctx.get('Authorization');
if (token) {
ctx.state.user = { id: 1, name: 'Alice' };
} else {
ctx.state.user = null;
}
await next();
});
// ビジネスロジック
app.use(async (ctx) => {
if (!ctx.state.user) {
ctx.throw(401, 'Unauthorized');
}
const results = await ctx.db.query('SELECT * FROM items');
ctx.body = { user: ctx.state.user, data: results };
});
app.listen(3000);
類似パッケージとの比較
| 特徴 | Koa | Express | Fastify | Hono |
|---|---|---|---|---|
| ミドルウェアモデル | オニオン(async/await) | 線形(コールバック) | プラグイン + フック | オニオン(async/await) |
| バンドルされるミドルウェア | なし(最小構成) | ルーティング・静的ファイル等 | スキーマバリデーション等 | ルーティング内蔵 |
| TypeScript サポート | @types/koa が必要 | @types/express が必要 | ネイティブ対応 | ネイティブ対応 |
| パフォーマンス | 良好 | 良好 | 非常に高速 | 非常に高速 |
| エコシステム | 豊富 | 最大規模 | 急成長中 | 成長中 |
| 設計思想 | 薄く・自由に組み合わせ | 実用的・オールインワン寄り | 高速・スキーマ駆動 | マルチランタイム対応 |
| 学習コスト | 低い | 低い | 中程度 | 低い |
選定の目安:
- Express からの移行で async/await を活かしたい → Koa
- パフォーマンス最優先 → Fastify / Hono
- エッジ環境やマルチランタイム → Hono
注意点・Tips
1. Koa v3 では v1 系ミドルウェアが動かない
v3 で Generator ベースのミドルウェア(function*(next) { ... })のサポートが完全に削除されました。古いミドルウェアを使う場合は、async/await 版にアップデートされているか確認してください。
2. ボディパーサーは別途必要
Koa 本体にはリクエストボディのパース機能がありません。POST/PUT リクエストを扱うには、ミドルウェアを追加します。
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
app.use(bodyParser());
app.use(async (ctx) => {
// ctx.request.body でパース済みボディにアクセス
console.log(ctx.request.body);
ctx.body = { received: true };
});
app.listen(3000);
3. ルーティングも別途必要
import Koa from 'koa';
import Router from '@koa/router';
const app = new Koa();
const router = new Router();
router.get('/users', async (ctx) => {
ctx.body = [{ id: 1, name: 'Alice' }];
});
router.get('/users/:id', async (ctx) => {
ctx.body = { id: ctx.params.id };
});
router.post('/users', async (ctx) => {
ctx.status = 201;
ctx.body = { created: true };
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
4. ctx.res.write() を直接使わない
Koa のレスポンス処理をバイパスしてしまうため、ctx.res(Node.js ネイティブの ServerResponse)を直接操作するのは避けてください。必ず ctx.body や ctx.response を通じてレスポンスを返しましょう。
5. エラーハンドラーは最初に登録する
オニオンモデルの特性上、エラーハンドラーミドルウェアは app.use() の最初に登録する必要があります。そうしないと、それより前に登録されたミドルウェアのエラーを捕捉できません。
// ✅ 正しい順序
app.use(errorHandler); // 最初
app.use(logger);
app.use(router.routes());
// ❌ 間違った順序
app.use(logger);
app.use(router.routes());
app.use(