api

Hono実践ガイド - 軽量・高速なエッジ対応Webフレームワーク

2025.12.02

Honoとは

Honoは、日本発の軽量・高速なWebフレームワークです。Cloudflare Workers、Deno、Bun、Node.js、AWS Lambdaなど、あらゆるJavaScriptランタイムで動作します。

flowchart TB
    subgraph Core["Hono Core (~12KB)"]
        Router["Router<br/>(RegExpRouter, TrieRouter)"]
        MW["Middleware Chain"]
        Ctx["Context<br/>(c.req, c.res)"]
    end

    Core --> CF["Cloudflare Workers"]
    Core --> Deno["Deno Deploy"]
    Core --> Bun["Bun"]

    CF --> Node["Node.js"]
    Deno --> Vercel["Vercel"]
    Bun --> Lambda["AWS Lambda"]

特徴: Web標準API準拠、TypeScript First、超軽量

セットアップ

# 新規プロジェクト作成
npm create hono@latest my-app

# 既存プロジェクトへの追加
npm install hono

# アダプター(必要に応じて)
npm install @hono/node-server

基本的なアプリケーション

// 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!' });
});

export default app;

ランタイム別の起動方法

// Cloudflare Workers
export default app;

// Node.js
import { serve } from '@hono/node-server';
serve(app);

// Bun
export default { port: 3000, fetch: app.fetch };

// Deno
Deno.serve(app.fetch);

ルーティング

import { Hono } from 'hono';

const app = new Hono();

// 基本的なルート
app.get('/users', (c) => c.json({ users: [] }));
app.post('/users', (c) => c.json({ created: true }));
app.put('/users/:id', (c) => c.json({ updated: true }));
app.delete('/users/:id', (c) => c.json({ deleted: true }));

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

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

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

// 正規表現パターン
app.get('/user/:id{[0-9]+}', (c) => {
  const id = c.req.param('id');
  return c.json({ id: parseInt(id) });
});

// オプショナルパラメータ
app.get('/posts/:id?', (c) => {
  const id = c.req.param('id');
  if (id) {
    return c.json({ post: { id } });
  }
  return c.json({ posts: [] });
});

ルートグループ

import { Hono } from 'hono';

const app = new Hono();

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

api.get('/users', (c) => c.json({ users: [] }));
api.get('/users/:id', (c) => c.json({ user: {} }));
api.post('/users', (c) => c.json({ created: true }));

// プレフィックス付きでマウント
app.route('/api', api);

// ネストしたルート
const v1 = new Hono();
const v2 = new Hono();

v1.get('/users', (c) => c.json({ version: 1 }));
v2.get('/users', (c) => c.json({ version: 2 }));

app.route('/api/v1', v1);
app.route('/api/v2', v2);

ミドルウェア

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

const app = new Hono();

// 組み込みミドルウェア
app.use('*', logger());
app.use('*', cors());
app.use('*', compress());
app.use('*', etag());
app.use('*', secureHeaders());
app.use('*', timing());
app.use('*', prettyJSON());

// カスタムミドルウェア
app.use('*', async (c, next) => {
  const start = Date.now();
  await next();
  const duration = Date.now() - start;
  c.res.headers.set('X-Response-Time', `${duration}ms`);
});

// 特定パスにのみ適用
app.use('/api/*', async (c, next) => {
  console.log('API request:', c.req.url);
  await next();
});

認証ミドルウェア

import { Hono } from 'hono';
import { jwt } from 'hono/jwt';
import { bearerAuth } from 'hono/bearer-auth';
import { basicAuth } from 'hono/basic-auth';

const app = new Hono();

// JWT認証
app.use('/api/*', jwt({
  secret: process.env.JWT_SECRET!,
}));

app.get('/api/me', (c) => {
  const payload = c.get('jwtPayload');
  return c.json({ user: payload });
});

// Bearer Token認証
app.use('/admin/*', bearerAuth({
  token: process.env.ADMIN_TOKEN!,
}));

// Basic認証
app.use('/dashboard/*', basicAuth({
  username: 'admin',
  password: 'secret',
}));

// カスタム認証
const authMiddleware = async (c: Context, next: Next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '');

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

  try {
    const user = await verifyToken(token);
    c.set('user', user);
    await next();
  } catch {
    return c.json({ error: 'Invalid token' }, 401);
  }
};

app.use('/api/*', authMiddleware);

バリデーション

Zodとの統合

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

const app = new Hono();

// リクエストボディのバリデーション
const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  age: z.number().int().positive().optional(),
});

app.post('/users', zValidator('json', createUserSchema), async (c) => {
  const data = c.req.valid('json');
  // data は型安全: { email: string, name: string, age?: number }
  return c.json({ user: data });
});

// クエリパラメータのバリデーション
const listUsersSchema = z.object({
  limit: z.coerce.number().int().min(1).max(100).default(10),
  offset: z.coerce.number().int().min(0).default(0),
  sort: z.enum(['name', 'created_at']).optional(),
});

app.get('/users', zValidator('query', listUsersSchema), async (c) => {
  const { limit, offset, sort } = c.req.valid('query');
  return c.json({ limit, offset, sort });
});

// パスパラメータのバリデーション
const userIdSchema = z.object({
  id: z.string().uuid(),
});

app.get('/users/:id', zValidator('param', userIdSchema), async (c) => {
  const { id } = c.req.valid('param');
  return c.json({ id });
});

// ヘッダーのバリデーション
const headerSchema = z.object({
  'x-api-key': z.string().min(32),
});

app.use('/api/*', zValidator('header', headerSchema));

カスタムバリデーションエラー

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

app.post(
  '/users',
  zValidator('json', createUserSchema, (result, c) => {
    if (!result.success) {
      return c.json(
        {
          error: {
            code: 'VALIDATION_ERROR',
            message: 'Invalid request body',
            details: result.error.issues.map((issue) => ({
              field: issue.path.join('.'),
              message: issue.message,
            })),
          },
        },
        400
      );
    }
  }),
  async (c) => {
    const data = c.req.valid('json');
    return c.json({ user: data });
  }
);

Hono RPC

型安全なクライアント-サーバー通信を実現します。

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

const app = new Hono()
  .get('/users', async (c) => {
    const users = await db.users.findMany();
    return c.json({ users });
  })
  .get('/users/:id', async (c) => {
    const id = c.req.param('id');
    const user = await db.users.findById(id);
    return c.json({ user });
  })
  .post(
    '/users',
    zValidator('json', z.object({
      email: z.string().email(),
      name: z.string(),
    })),
    async (c) => {
      const data = c.req.valid('json');
      const user = await db.users.create(data);
      return c.json({ user }, 201);
    }
  );

export type AppType = typeof app;
export default app;
// client.ts
import { hc } from 'hono/client';
import type { AppType } from './server';

const client = hc<AppType>('http://localhost:3000');

// 型安全なAPI呼び出し
async function main() {
  // GET /users
  const res1 = await client.users.$get();
  const data1 = await res1.json(); // 型推論される

  // GET /users/:id
  const res2 = await client.users[':id'].$get({
    param: { id: '123' },
  });
  const data2 = await res2.json();

  // POST /users
  const res3 = await client.users.$post({
    json: {
      email: 'user@example.com',
      name: 'John Doe',
    },
  });
  const data3 = await res3.json();
}

ファイルアップロード

import { Hono } from 'hono';

const app = new Hono();

app.post('/upload', async (c) => {
  const body = await c.req.parseBody();
  const file = body['file'];

  if (file instanceof File) {
    const buffer = await file.arrayBuffer();
    const filename = file.name;
    const contentType = file.type;
    const size = file.size;

    // S3やR2にアップロード
    await uploadToStorage(buffer, filename);

    return c.json({
      filename,
      contentType,
      size,
      url: `https://cdn.example.com/${filename}`,
    });
  }

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

// 複数ファイルアップロード
app.post('/upload-multiple', async (c) => {
  const body = await c.req.parseBody({ all: true });
  const files = body['files'];

  if (Array.isArray(files)) {
    const results = await Promise.all(
      files.map(async (file) => {
        if (file instanceof File) {
          const buffer = await file.arrayBuffer();
          await uploadToStorage(buffer, file.name);
          return { filename: file.name, size: file.size };
        }
        return null;
      })
    );

    return c.json({ uploaded: results.filter(Boolean) });
  }

  return c.json({ error: 'No files uploaded' }, 400);
});

エラーハンドリング

import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';

const app = new Hono();

// カスタムエラークラス
class NotFoundError extends HTTPException {
  constructor(resource: string) {
    super(404, { message: `${resource} not found` });
  }
}

class ValidationError extends HTTPException {
  constructor(details: unknown[]) {
    super(400, {
      message: 'Validation failed',
      cause: { details },
    });
  }
}

// エラーハンドラー
app.onError((err, c) => {
  console.error('Error:', err);

  if (err instanceof HTTPException) {
    return c.json(
      {
        error: {
          code: err.status === 404 ? 'NOT_FOUND' : 'ERROR',
          message: err.message,
          details: err.cause,
        },
      },
      err.status
    );
  }

  return c.json(
    {
      error: {
        code: 'INTERNAL_ERROR',
        message: 'An unexpected error occurred',
      },
    },
    500
  );
});

// 404ハンドラー
app.notFound((c) => {
  return c.json(
    {
      error: {
        code: 'NOT_FOUND',
        message: 'Route not found',
      },
    },
    404
  );
});

// 使用例
app.get('/users/:id', async (c) => {
  const user = await db.users.findById(c.req.param('id'));
  if (!user) {
    throw new NotFoundError('User');
  }
  return c.json({ user });
});

Cloudflare Workers連携

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

type Bindings = {
  DB: D1Database;
  KV: KVNamespace;
  R2: R2Bucket;
  AI: Ai;
};

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

// D1データベース
app.get('/users', async (c) => {
  const { results } = await c.env.DB.prepare(
    'SELECT * FROM users LIMIT 10'
  ).all();
  return c.json({ users: results });
});

// KVストレージ
app.get('/cache/:key', async (c) => {
  const key = c.req.param('key');
  const value = await c.env.KV.get(key);
  return c.json({ key, value });
});

app.put('/cache/:key', async (c) => {
  const key = c.req.param('key');
  const { value, ttl } = await c.req.json();
  await c.env.KV.put(key, value, { expirationTtl: ttl });
  return c.json({ success: true });
});

// R2オブジェクトストレージ
app.get('/files/:key', async (c) => {
  const key = c.req.param('key');
  const object = await c.env.R2.get(key);

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

  const headers = new Headers();
  object.writeHttpMetadata(headers);
  headers.set('etag', object.httpEtag);

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

// Workers AI
app.post('/ai/summarize', async (c) => {
  const { text } = await c.req.json();

  const response = await c.env.AI.run('@cf/meta/llama-2-7b-chat-int8', {
    messages: [
      { role: 'system', content: 'Summarize the following text.' },
      { role: 'user', content: text },
    ],
  });

  return c.json({ summary: response.response });
});

export default app;

テスト

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

describe('API Tests', () => {
  it('GET / returns Hello', async () => {
    const res = await app.request('/');
    expect(res.status).toBe(200);
    expect(await res.text()).toBe('Hello Hono!');
  });

  it('GET /users returns user list', async () => {
    const res = await app.request('/users');
    expect(res.status).toBe(200);

    const data = await res.json();
    expect(data).toHaveProperty('users');
  });

  it('POST /users creates a user', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: 'test@example.com',
        name: 'Test User',
      }),
    });

    expect(res.status).toBe(201);
    const data = await res.json();
    expect(data.user.email).toBe('test@example.com');
  });

  it('POST /users returns 400 for invalid data', async () => {
    const res = await app.request('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: 'invalid-email',
      }),
    });

    expect(res.status).toBe(400);
  });
});

参考リンク

← 一覧に戻る