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