Hono完全ガイド — 軽量・高速WebフレームワークのEdge時代の選択肢


Honoは、Edge Runtimeに最適化された超軽量・高速なWebフレームワークです。Cloudflare Workers、Deno、Bun、Node.jsなど、あらゆるJavaScriptランタイムで動作し、Express.jsのようなシンプルなAPIを提供しながら、圧倒的なパフォーマンスを実現します。この記事では、Honoの基本から実践的な使い方まで徹底的に解説します。

Honoとは

Honoは「炎」を意味する日本語から名付けられた、超高速なWebフレームワークです。主な特徴は以下の通りです。

  • 超軽量 - 依存関係ゼロ、わずか13KB(gzip後)
  • 超高速 - RegExpベースのルーターで最速クラスの性能
  • マルチランタイム対応 - Cloudflare Workers、Deno、Bun、Node.js、Fastly Compute@Edge、Vercel Edge Functions
  • 型安全 - TypeScriptファーストで完全な型推論
  • ミドルウェア豊富 - 認証、CORS、キャッシュ、圧縮など標準装備
  • Express互換API - 学習コストが低い

基本的な使い方

インストールとセットアップ

# Cloudflare Workers向け
npm create hono@latest my-app
cd my-app
npm install

# または手動インストール
npm install hono

最小構成のAPI

// src/index.ts
import { Hono } from 'hono';

const app = new Hono();

app.get('/', (c) => {
  return c.text('Hello Hono!');
});

app.get('/json', (c) => {
  return c.json({ message: 'Hello JSON!' });
});

app.get('/html', (c) => {
  return c.html('<h1>Hello HTML!</h1>');
});

export default app;

Cloudflare Workersで実行:

npm run dev
# http://localhost:8787 でアクセス可能

基本的なルーティング

import { Hono } from 'hono';

const app = new Hono();

// GETリクエスト
app.get('/users', (c) => c.json({ users: [] }));

// POSTリクエスト
app.post('/users', async (c) => {
  const body = await c.req.json();
  return c.json({ created: body }, 201);
});

// PUTリクエスト
app.put('/users/:id', async (c) => {
  const id = c.req.param('id');
  const body = await c.req.json();
  return c.json({ id, updated: body });
});

// DELETEリクエスト
app.delete('/users/:id', (c) => {
  const id = c.req.param('id');
  return c.json({ deleted: id }, 204);
});

// PATCHリクエスト
app.patch('/users/:id', async (c) => {
  const id = c.req.param('id');
  const body = await c.req.json();
  return c.json({ id, patched: body });
});

export default app;

パスパラメータとクエリパラメータ

パスパラメータ

// 単一パラメータ
app.get('/users/:id', (c) => {
  const id = c.req.param('id');
  return c.json({ userId: id });
});

// 複数パラメータ
app.get('/posts/:postId/comments/:commentId', (c) => {
  const postId = c.req.param('postId');
  const commentId = c.req.param('commentId');
  return c.json({ postId, commentId });
});

// ワイルドカード
app.get('/files/*', (c) => {
  const path = c.req.param('*');
  return c.text(`File path: ${path}`);
});

// 正規表現パターン
app.get('/posts/:id{[0-9]+}', (c) => {
  const id = c.req.param('id'); // 数字のみマッチ
  return c.json({ postId: id });
});

クエリパラメータ

app.get('/search', (c) => {
  // 単一パラメータ
  const q = c.req.query('q');

  // 複数パラメータ
  const page = c.req.query('page') || '1';
  const limit = c.req.query('limit') || '10';

  // 配列パラメータ(?tags=js&tags=ts)
  const tags = c.req.queries('tags');

  return c.json({
    query: q,
    page: parseInt(page),
    limit: parseInt(limit),
    tags,
  });
});

リクエストボディの処理

// JSONボディ
app.post('/api/users', async (c) => {
  const body = await c.req.json();
  return c.json({ received: body });
});

// テキストボディ
app.post('/api/text', async (c) => {
  const text = await c.req.text();
  return c.text(`Received: ${text}`);
});

// FormData
app.post('/api/upload', async (c) => {
  const formData = await c.req.formData();
  const file = formData.get('file');
  const name = formData.get('name');
  return c.json({ fileName: file?.name, name });
});

// ArrayBuffer
app.post('/api/binary', async (c) => {
  const buffer = await c.req.arrayBuffer();
  return c.json({ size: buffer.byteLength });
});

// Raw Request
app.post('/api/raw', async (c) => {
  const req = c.req.raw;
  const contentType = req.headers.get('content-type');
  return c.json({ contentType });
});

型安全なルーティング

Honoの最大の特徴の一つが、完全な型推論です。

import { Hono } from 'hono';

// 型定義
type User = {
  id: number;
  name: string;
  email: string;
};

type CreateUserInput = Omit<User, 'id'>;

const app = new Hono();

// 型安全なレスポンス
app.get('/users/:id', (c) => {
  const id = c.req.param('id');

  const user: User = {
    id: parseInt(id),
    name: 'John Doe',
    email: 'john@example.com',
  };

  // cは型推論される
  return c.json(user);
});

// 型安全なリクエスト
app.post('/users', async (c) => {
  const input: CreateUserInput = await c.req.json();

  const user: User = {
    id: Date.now(),
    ...input,
  };

  return c.json(user, 201);
});

export default app;

Zodによるバリデーション

import { Hono } from 'hono';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';

const app = new Hono();

// スキーマ定義
const userSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
});

// バリデーションミドルウェア
app.post('/users', zValidator('json', userSchema), async (c) => {
  // バリデーション済みのデータを取得
  const data = c.req.valid('json');

  // ここでdataは型安全に扱える
  return c.json({
    message: 'User created',
    user: data,
  }, 201);
});

// クエリパラメータのバリデーション
const searchSchema = z.object({
  q: z.string().min(1),
  page: z.string().regex(/^\d+$/).transform(Number).optional(),
  limit: z.string().regex(/^\d+$/).transform(Number).optional(),
});

app.get('/search', zValidator('query', searchSchema), (c) => {
  const { q, page = 1, limit = 10 } = c.req.valid('query');

  return c.json({
    query: q,
    page,
    limit,
    results: [],
  });
});

export default app;

ミドルウェアの活用

組み込みミドルウェア

import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { prettyJSON } from 'hono/pretty-json';
import { etag } from 'hono/etag';
import { compress } from 'hono/compress';

const app = new Hono();

// CORSミドルウェア
app.use('/*', cors({
  origin: ['https://example.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,
  credentials: true,
}));

// ロガー
app.use('*', logger());

// 整形されたJSON出力(開発時のみ)
app.use('*', prettyJSON());

// ETag生成
app.use('/api/*', etag());

// レスポンス圧縮
app.use('*', compress());

export default app;

カスタムミドルウェア

import { Hono } from 'hono';
import type { Context, Next } from 'hono';

const app = new Hono();

// リクエストIDミドルウェア
const requestId = () => {
  return async (c: Context, next: Next) => {
    const id = crypto.randomUUID();
    c.set('requestId', id);
    c.header('X-Request-ID', id);
    await next();
  };
};

// タイミングミドルウェア
const timing = () => {
  return async (c: Context, next: Next) => {
    const start = Date.now();
    await next();
    const duration = Date.now() - start;
    c.header('X-Response-Time', `${duration}ms`);
  };
};

// 認証ミドルウェア
const auth = () => {
  return async (c: Context, next: Next) => {
    const token = c.req.header('Authorization')?.replace('Bearer ', '');

    if (!token) {
      return c.json({ error: 'Unauthorized' }, 401);
    }

    // トークン検証(簡易版)
    if (token !== 'secret-token') {
      return c.json({ error: 'Invalid token' }, 401);
    }

    c.set('userId', 'user-123');
    await next();
  };
};

// グローバルに適用
app.use('*', requestId());
app.use('*', timing());

// 特定のルートのみ
app.use('/api/*', auth());

app.get('/api/me', (c) => {
  const userId = c.get('userId');
  const requestId = c.get('requestId');
  return c.json({ userId, requestId });
});

export default app;

JWT認証の実装

import { Hono } from 'hono';
import { jwt, sign } from 'hono/jwt';

const app = new Hono();

const SECRET = 'your-secret-key';

// ログインエンドポイント
app.post('/login', async (c) => {
  const { username, password } = await c.req.json();

  // 認証ロジック(簡易版)
  if (username === 'admin' && password === 'password') {
    const payload = {
      sub: username,
      role: 'admin',
      exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1時間
    };

    const token = await sign(payload, SECRET);
    return c.json({ token });
  }

  return c.json({ error: 'Invalid credentials' }, 401);
});

// JWT認証が必要なルート
app.use('/api/*', jwt({ secret: SECRET }));

app.get('/api/profile', (c) => {
  const payload = c.get('jwtPayload');
  return c.json({
    username: payload.sub,
    role: payload.role,
  });
});

export default app;

エラーハンドリング

import { Hono } from 'hono';
import type { ErrorHandler } from 'hono';

const app = new Hono();

// カスタムエラークラス
class AppError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public code?: string
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// グローバルエラーハンドラ
const errorHandler: ErrorHandler = (err, c) => {
  console.error(`[Error] ${err.message}`);

  if (err instanceof AppError) {
    return c.json({
      error: err.message,
      code: err.code,
    }, err.statusCode);
  }

  return c.json({
    error: 'Internal Server Error',
  }, 500);
};

app.onError(errorHandler);

// エラーを投げる例
app.get('/users/:id', async (c) => {
  const id = c.req.param('id');

  if (!id.match(/^\d+$/)) {
    throw new AppError('Invalid user ID', 400, 'INVALID_ID');
  }

  // ユーザー取得ロジック
  const user = null; // 仮

  if (!user) {
    throw new AppError('User not found', 404, 'USER_NOT_FOUND');
  }

  return c.json({ user });
});

// 404ハンドラ
app.notFound((c) => {
  return c.json({
    error: 'Not Found',
    path: c.req.path,
  }, 404);
});

export default app;

データベース統合

Cloudflare D1(SQLite)

import { Hono } from 'hono';

type Bindings = {
  DB: D1Database;
};

const app = new Hono<{ Bindings: Bindings }>();

// ユーザー一覧取得
app.get('/users', async (c) => {
  const { results } = await c.env.DB.prepare(
    'SELECT * FROM users ORDER BY created_at DESC'
  ).all();

  return c.json({ users: results });
});

// ユーザー作成
app.post('/users', async (c) => {
  const { name, email } = await c.req.json();

  const { success } = await c.env.DB.prepare(
    'INSERT INTO users (name, email) VALUES (?, ?)'
  ).bind(name, email).run();

  if (success) {
    return c.json({ message: 'User created' }, 201);
  }

  return c.json({ error: 'Failed to create user' }, 500);
});

// ユーザー取得
app.get('/users/:id', async (c) => {
  const id = c.req.param('id');

  const user = await c.env.DB.prepare(
    'SELECT * FROM users WHERE id = ?'
  ).bind(id).first();

  if (!user) {
    return c.json({ error: 'User not found' }, 404);
  }

  return c.json({ user });
});

export default app;

Prisma統合(Node.js/Bun)

import { Hono } from 'hono';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const app = new Hono();

app.get('/posts', async (c) => {
  const posts = await prisma.post.findMany({
    include: {
      author: true,
    },
    orderBy: {
      createdAt: 'desc',
    },
  });

  return c.json({ posts });
});

app.post('/posts', async (c) => {
  const { title, content, authorId } = await c.req.json();

  const post = await prisma.post.create({
    data: {
      title,
      content,
      authorId,
    },
  });

  return c.json({ post }, 201);
});

app.get('/posts/:id', async (c) => {
  const id = parseInt(c.req.param('id'));

  const post = await prisma.post.findUnique({
    where: { id },
    include: { author: true },
  });

  if (!post) {
    return c.json({ error: 'Post not found' }, 404);
  }

  return c.json({ post });
});

export default app;

キャッシング戦略

import { Hono } from 'hono';
import { cache } from 'hono/cache';

const app = new Hono();

// 静的キャッシュ(1時間)
app.get(
  '/api/static',
  cache({
    cacheName: 'my-app',
    cacheControl: 'max-age=3600',
  }),
  (c) => {
    return c.json({
      timestamp: Date.now(),
      data: 'This is cached',
    });
  }
);

// Cloudflare KVを使ったカスタムキャッシュ
type Bindings = {
  CACHE: KVNamespace;
};

const appWithKV = new Hono<{ Bindings: Bindings }>();

appWithKV.get('/api/data/:id', async (c) => {
  const id = c.req.param('id');
  const cacheKey = `data:${id}`;

  // キャッシュチェック
  const cached = await c.env.CACHE.get(cacheKey, 'json');
  if (cached) {
    return c.json({ ...cached, cached: true });
  }

  // データ取得(重い処理を想定)
  const data = {
    id,
    value: Math.random(),
    timestamp: Date.now(),
  };

  // キャッシュに保存(1時間)
  await c.env.CACHE.put(cacheKey, JSON.stringify(data), {
    expirationTtl: 3600,
  });

  return c.json({ ...data, cached: false });
});

export default appWithKV;

ファイルアップロード

import { Hono } from 'hono';

type Bindings = {
  BUCKET: R2Bucket;
};

const app = new Hono<{ Bindings: Bindings }>();

// Cloudflare R2へのファイルアップロード
app.post('/upload', async (c) => {
  const formData = await c.req.formData();
  const file = formData.get('file') as File;

  if (!file) {
    return c.json({ error: 'No file provided' }, 400);
  }

  // ファイルサイズチェック(10MB制限)
  if (file.size > 10 * 1024 * 1024) {
    return c.json({ error: 'File too large' }, 400);
  }

  // ファイルタイプチェック
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
  if (!allowedTypes.includes(file.type)) {
    return c.json({ error: 'Invalid file type' }, 400);
  }

  // ファイル名生成
  const fileName = `${crypto.randomUUID()}-${file.name}`;

  // R2にアップロード
  await c.env.BUCKET.put(fileName, file.stream(), {
    httpMetadata: {
      contentType: file.type,
    },
  });

  return c.json({
    message: 'File uploaded',
    fileName,
    url: `/files/${fileName}`,
  }, 201);
});

// ファイル取得
app.get('/files/:name', async (c) => {
  const name = c.req.param('name');

  const object = await c.env.BUCKET.get(name);

  if (!object) {
    return c.json({ error: 'File not found' }, 404);
  }

  const headers = new Headers();
  object.writeHttpMetadata(headers);
  headers.set('Cache-Control', 'public, max-age=31536000');

  return new Response(object.body, { headers });
});

export default app;

ルートグルーピング

import { Hono } from 'hono';

const app = new Hono();

// APIルートグループ
const api = new Hono();

api.get('/users', (c) => c.json({ users: [] }));
api.post('/users', async (c) => {
  const body = await c.req.json();
  return c.json({ created: body }, 201);
});

// 管理者ルートグループ
const admin = new Hono();

admin.use('*', async (c, next) => {
  // 認証チェック
  const isAdmin = c.req.header('X-Admin-Token') === 'secret';
  if (!isAdmin) {
    return c.json({ error: 'Forbidden' }, 403);
  }
  await next();
});

admin.get('/stats', (c) => c.json({ stats: {} }));
admin.delete('/users/:id', (c) => {
  const id = c.req.param('id');
  return c.json({ deleted: id });
});

// グループをマウント
app.route('/api', api);
app.route('/admin', admin);

export default app;

WebSocketサポート(Cloudflare Workers)

import { Hono } from 'hono';

const app = new Hono();

app.get('/ws', async (c) => {
  const upgradeHeader = c.req.header('Upgrade');

  if (upgradeHeader !== 'websocket') {
    return c.text('Expected Upgrade: websocket', 426);
  }

  const webSocketPair = new WebSocketPair();
  const [client, server] = Object.values(webSocketPair);

  server.accept();

  server.addEventListener('message', (event) => {
    console.log('Received:', event.data);
    server.send(`Echo: ${event.data}`);
  });

  server.addEventListener('close', () => {
    console.log('WebSocket closed');
  });

  return new Response(null, {
    status: 101,
    webSocket: client,
  });
});

export default app;

テスト

// test/index.test.ts
import { describe, it, expect } from 'vitest';
import app from '../src/index';

describe('API Tests', () => {
  it('GET / should return hello message', async () => {
    const res = await app.request('http://localhost/');
    expect(res.status).toBe(200);

    const text = await res.text();
    expect(text).toBe('Hello Hono!');
  });

  it('POST /users should create user', async () => {
    const res = await app.request('http://localhost/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: 'John Doe',
        email: 'john@example.com',
      }),
    });

    expect(res.status).toBe(201);

    const json = await res.json();
    expect(json.created.name).toBe('John Doe');
  });

  it('GET /api/me should require authentication', async () => {
    const res = await app.request('http://localhost/api/me');
    expect(res.status).toBe(401);
  });
});

デプロイ

Cloudflare Workers

# wrangler.tomlの設定
npm run deploy
# wrangler.toml
name = "my-hono-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[env.production]
vars = { ENVIRONMENT = "production" }

Vercel Edge Functions

// api/index.ts
import { Hono } from 'hono';
import { handle } from 'hono/vercel';

const app = new Hono().basePath('/api');

app.get('/hello', (c) => c.json({ message: 'Hello from Vercel!' }));

export const GET = handle(app);
export const POST = handle(app);

Deno Deploy

// main.ts
import { Hono } from 'https://deno.land/x/hono/mod.ts';

const app = new Hono();

app.get('/', (c) => c.text('Hello Deno!'));

Deno.serve(app.fetch);

パフォーマンス最適化

import { Hono } from 'hono';
import { compress } from 'hono/compress';
import { etag } from 'hono/etag';

const app = new Hono();

// レスポンス圧縮
app.use('*', compress());

// ETag生成
app.use('*', etag());

// ストリーミングレスポンス
app.get('/stream', (c) => {
  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue('chunk1\n');
      controller.enqueue('chunk2\n');
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/plain',
      'Transfer-Encoding': 'chunked',
    },
  });
});

export default app;

まとめ

Honoは、Edge Computing時代の理想的なWebフレームワークです。

主な利点:

  • 超軽量で高速なパフォーマンス
  • あらゆるランタイムで動作するポータビリティ
  • TypeScriptファーストの型安全性
  • 豊富なミドルウェアエコシステム
  • Express互換の学習しやすいAPI

Cloudflare Workers、Deno Deploy、Vercel Edge FunctionsなどのEdge Runtimeで高速なAPIを構築したい場合、Honoは最高の選択肢の一つです。軽量でありながら、本格的なアプリケーション開発に必要な機能が全て揃っています。