koa の使い方

Koa web app framework

v3.2.0/週MITWeb API
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

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);

類似パッケージとの比較

特徴KoaExpressFastifyHono
ミドルウェアモデルオニオン(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.bodyctx.response を通じてレスポンスを返しましょう。

5. エラーハンドラーは最初に登録する

オニオンモデルの特性上、エラーハンドラーミドルウェアは app.use()最初に登録する必要があります。そうしないと、それより前に登録されたミドルウェアのエラーを捕捉できません。

// ✅ 正しい順序
app.use(errorHandler);   // 最初
app.use(logger);
app.use(router.routes());

// ❌ 間違った順序
app.use(logger);
app.use(router.routes());
app.use(

比較記事