Hono = Lightweight Web Framework for Edge
Yusuke Wada - 日本人エンジニア
req/sec (リクエスト/秒) の比較:
Hono ████████████████████ 20,000 req/s
Fastify ███████████████ 15,000 req/s
Express ██████ 6,000 req/s
理由:
• Hono: Router 最適化
• Fastify: JSON パース高速化
• Express: 古い設計、ミドルウェアチェーン遅い
| 項目 | Hono | Express | Fastify |
|---|---|---|---|
| 学習難易度 | 簡単 | 簡単 | 中程度 |
| Edgeランタイム対応 | ✅ | ❌ | ❌ |
| バンドルサイズ | 64 KB | 200 KB | 150 KB |
| TypeScript | First-class | 別途セットアップ | First-class |
| ミドルウェア数 | 豊富 | 超豊富 | 豊富 |
| 企業採用 | 新興 | 企業多い | 企業増加中 |
| 日本ドキュメント | 豊富 | 豊富 | 少ない |
Hono を選ぶべき:
✅ Edge 環境で動かしたい (Cloudflare Workers / Vercel Edge)
✅ 小さく・高速にしたい
✅ TypeScript を完全に使いたい
✅ 新しいプロジェクト
Express を選ぶべき:
✅ 既存コード
✅ プラグインの豊富さが必須
✅ ミドルウェアの互換性が必須
Fastify を選ぶべき:
✅ JSON API で極限のパフォーマンスが必須
✅ JOI / Joi バリデーション使い慣れている
# Node.js プロジェクト
npm create hono my-app -- --template nodejs
cd my-app
npm install
npm run dev
# Cloudflare Workers
npm create hono my-app -- --template cloudflare-workers
# Deno
deno run --allow-net https://examples.hono.dev/hello
// server.ts
import { Hono } from 'hono';
const app = new Hono();
// ✅ シンプル
app.get('/', (c) => c.text('Hello, World!'));
export default app;
app.get('/users/:id', (c) => {
// URL パース
const id = c.req.param('id'); // :id の値
// Query パラメータ
const sort = c.req.query('sort'); // ?sort=name
const queries = c.req.queries('tag'); // ?tag=a&tag=b
// Header
const auth = c.req.header('Authorization');
// Body
const body = await c.req.json();
const form = await c.req.formData();
// Cookie
const sessionId = getCookie(c, 'session_id');
// レスポンス
return c.json({ id }, 200);
return c.text('Hello');
return c.html('<h1>Hello</h1>');
return c.redirect('/new-path');
});
import { Hono } from 'hono';
const app = new Hono();
// GET
app.get('/articles', (c) => c.json({ articles: [] }));
// POST
app.post('/articles', (c) => c.json({ created: true }, 201));
// PUT / PATCH / DELETE
app.put('/articles/:id', (c) => /* ... */);
app.delete('/articles/:id', (c) => /* ... */);
// 複数メソッド
app.all('/admin/*', (c) => c.json({ admin: true }));
// ルーターグループ
const api = new Hono();
api.get('/users', (c) => /* ... */);
app.route('/api', api);
// → GET /api/users
// 1️⃣ グローバルミドルウェア
app.use(async (c, next) => {
console.log(`${c.req.method} ${c.req.path}`);
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(`Duration: ${duration}ms`);
});
// 2️⃣ 認証ミドルウェア
app.use('/admin/*', async (c, next) => {
const auth = c.req.header('Authorization');
if (!auth?.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401);
}
const token = auth.slice(7);
// トークン検証
c.set('userId', 'user123'); // Context に値セット
await next();
});
app.get('/admin/dashboard', (c) => {
const userId = c.get('userId');
return c.json({ dashboard: userId });
});
// 3️⃣ エラーハンドリング
app.onError((err, c) => {
console.error(err);
return c.json({ error: 'Internal Server Error' }, 500);
});
import { Hono } from 'hono';
const app = new Hono();
const requestMap = new Map<string, number[]>();
app.use(async (c, next) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown';
const now = Date.now();
// 最後1分以内のリクエスト取得
const timestamps = requestMap.get(ip) || [];
const recent = timestamps.filter(t => now - t < 60000);
// 制限越えたら拒否
if (recent.length >= 100) {
return c.json({ error: 'Too Many Requests' }, 429);
}
// リクエスト記録
recent.push(now);
requestMap.set(ip, recent);
await next();
});
app.get('/articles', (c) => {
return c.json({ articles: [] });
});
export default app;
Frontend: https://example.vercel.app
Backend: https://api.example.com
【Next.js - lib/api.ts】
export async function getArticles() {
const res = await fetch('https://api.example.com/articles', {
headers: { 'Authorization': `Bearer ${process.env.API_KEY}` }
});
return res.json();
}
【Hono Backend - server.ts】
import { Hono } from 'hono';
import { cors } from 'hono/cors';
const app = new Hono();
app.use(cors({
origin: 'https://example.vercel.app',
credentials: true,
}));
app.get('/articles', (c) => {
// DB から取得
return c.json({ articles: [] });
});
export default app;
【pages/api/[...route].ts】
export { default } from '@/server';
【server.ts】
import { Hono } from 'hono';
import { handle } from 'hono/vercel';
const app = new Hono().basePath('/api');
app.get('/articles', (c) => {
return c.json({ articles: [] });
});
export default handle(app);
→ Deploy して https://example.vercel.app/api/articles にアクセス
【Backend - server.ts】
import { Hono } from 'hono';
import { z } from 'zod';
import supabase from './lib/supabase';
const app = new Hono();
// スキーマ定義
const createArticleSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
status: z.enum(['draft', 'published']),
});
// 記事一覧取得
app.get('/articles', async (c) => {
const status = c.req.query('status') || 'published';
const { data, error } = await supabase
.from('articles')
.select('id, title, created_at')
.eq('status', status)
.order('created_at', { ascending: false });
if (error) {
return c.json({ error: error.message }, 500);
}
return c.json({ articles: data });
});
// 記事作成
app.post('/articles', async (c) => {
const body = await c.req.json();
// バリデーション
const result = createArticleSchema.safeParse(body);
if (!result.success) {
return c.json({ error: result.error.flatten() }, 400);
}
const { title, content, status } = result.data;
const { data, error } = await supabase
.from('articles')
.insert({ title, content, status })
.select()
.single();
if (error) {
return c.json({ error: error.message }, 500);
}
return c.json({ article: data }, 201);
});
// 記事更新
app.put('/articles/:id', async (c) => {
const id = c.req.param('id');
const body = await c.req.json();
const result = createArticleSchema.partial().safeParse(body);
if (!result.success) {
return c.json({ error: result.error.flatten() }, 400);
}
const { data, error } = await supabase
.from('articles')
.update(result.data)
.eq('id', id)
.select()
.single();
if (error) {
return c.json({ error: error.message }, 500);
}
return c.json({ article: data });
});
// 記事削除
app.delete('/articles/:id', async (c) => {
const id = c.req.param('id');
const { error } = await supabase
.from('articles')
.delete()
.eq('id', id);
if (error) {
return c.json({ error: error.message }, 500);
}
return c.json({ message: 'Deleted' });
});
export default app;
【Frontend - app/articles/page.tsx】
'use client';
import { useEffect, useState } from 'react';
interface Article {
id: string;
title: string;
created_at: string;
}
export default function ArticlesPage() {
const [articles, setArticles] = useState<Article[]>([]);
useEffect(() => {
async function fetchArticles() {
const res = await fetch('/api/articles?status=published');
const { articles } = await res.json();
setArticles(articles);
}
fetchArticles();
}, []);
return (
<div>
{articles.map(article => (
<div key={article.id}>
<h2>{article.title}</h2>
<p>{new Date(article.created_at).toLocaleDateString()}</p>
</div>
))}
</div>
);
}
# 1. Railway プロジェクト作成
railway init
# 2. デプロイ設定
# railway.json
{
"build": {
"builder": "nixpacks",
"cmd": "npm run build"
},
"deploy": {
"cmd": "npm start"
}
}
# 3. デプロイ
railway up
【wrangler.toml】
name = "my-hono-api"
type = "javascript"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[env.production]
route = "api.example.com/*"
zone_id = "your_zone_id"
【src/index.ts】
import { Hono } from 'hono';
const app = new Hono();
app.get('/articles', (c) => {
return c.json({ articles: [] });
});
export default app;
# デプロイ
npm run deploy
【api/articles.ts】
import { Hono } from 'hono';
import { handle } from 'hono/vercel';
const app = new Hono();
app.get('/', (c) => {
return c.json({ articles: [] });
});
export const config = {
runtime: 'edge',
};
export default handle(app);
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { bearerAuth } from 'hono/bearer-auth';
import { compress } from 'hono/compress';
import { jwt } from 'hono/jwt';
const app = new Hono();
// ログ出力
app.use(logger());
// CORS
app.use(cors({ origin: 'https://example.com' }));
// 圧縮
app.use(compress());
// Bearer Token 認証
app.use(bearerAuth({ token: 'secret-token' }));
// JWT 認証
app.use(jwt({ secret: 'your-secret' }));
export default app;
import { Hono } from 'hono';
import { z } from 'zod';
const app = new Hono();
// ✅ 1. スキーマ分離
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
// ✅ 2. 型安全
app.post('/login', async (c) => {
const body = await c.req.json();
const result = userSchema.safeParse(body);
if (!result.success) return c.json({ error: result.error }, 400);
return c.json({ token: 'xxx' });
});
// ✅ 3. エラーハンドリング
app.onError((err, c) => {
console.error(err);
return c.json({ error: 'Internal Error' }, 500);
});
// ✅ 4. 404 Handler
app.notFound((c) => c.json({ error: 'Not Found' }, 404));
export default app;