hono/clientを使うとAPIの型補完が効いてタイポ・変更漏れに気づけるHono(炎🔥)は、日本人エンジニアが作ったWeb向け軽量フレームワーク。
「どのJS実行環境でも動く、超軽量・高速なWebフレームワーク」が一言定義。
| 特徴 | 内容 |
|---|---|
| 軽量・高速 | コアが約14KB。起動が速い |
| マルチランタイム | Cloudflare Workers / Node.js / Deno / Bun など複数環境で動く |
| TypeScriptファースト | 型が最初から整備されており、補完が効く |
Django
├── フルスタック(ORM・認証・管理画面・テンプレートエンジン込み)
├── 「全部入り」の安心感
└── 起動・レスポンスは比較的重め
Hono
├── ルーティングとミドルウェアに集中した「薄い」フレームワーク
├── ORMや認証は自分で選んで組み合わせる
└── とにかく速い・軽い
// Express.js
import express from 'express'
const app = express()
app.get('/hello', (req, res) => {
res.json({ message: 'Hello!' })
})
app.listen(3000)
// Hono(ほぼ同じ感覚で書ける)
import { Hono } from 'hono'
const app = new Hono()
app.get('/hello', (c) => {
return c.json({ message: 'Hello!' })
})
export default app // どの環境でもそのまま動く
💡 ポイント: DjangoはORMや管理画面が必要なプロジェクト向け。HonoはAPIサーバーをシンプルに、かつ型安全に作りたいときの選択肢。
| 項目 | Next.js API Routes | Express.js | Hono |
|---|---|---|---|
| 型安全 | △ 手動 | △ 手動 | ✅ 組み込み |
| バリデーション | ❌ 自前 | ❌ 自前 | ✅ Zod統合 |
| ルーティング管理 | △ ファイルベース | ✅ 自由 | ✅ 自由 |
| Next.jsからの独立性 | ❌ 依存 | ✅ 独立 | ✅ 独立 |
| 実行速度 | 普通 | 普通 | 速い |
| マルチランタイム | ❌ | ❌ | ✅ |
| 構成 | 向いているケース |
|---|---|
| Next.js + Django | 既存Django運用中・管理画面が必要 |
| Next.js + Hono | 新規・型安全重視・TypeScript統一 |
| Next.js + Hono + Django | 管理画面必要 + 新規API開発 |
💡 ポイント: Next.js API Routesは小規模なら十分。アプリが育ってきたとき・モバイル対応したいとき・型安全を徹底したいときにHonoの強みが光る。
import { Hono } from 'hono'
const app = new Hono()
// 基本的なHTTPメソッド
app.get('/users', (c) => c.json({ users: [] }))
app.post('/users', (c) => c.json({ created: true }, 201))
app.put('/users/:id', (c) => c.json({ updated: true }))
app.delete('/users/:id', (c) => c.json({ deleted: true }))
// URLパラメータ
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id })
})
// クエリパラメータ (?page=1&limit=10)
app.get('/posts', (c) => {
const page = c.req.query('page')
const limit = c.req.query('limit')
return c.json({ page, limit })
})
ルートのグループ化(urls.pyに相当)
// routes/users.ts
export const usersRoute = new Hono()
.get('/', (c) => c.json({ users: [] }))
.post('/', (c) => c.json({ created: true }, 201))
.get('/:id', (c) => c.json({ id: c.req.param('id') }))
// server/index.ts
const app = new Hono()
.route('/users', usersRoute)
.route('/posts', postsRoute)
export default app
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono()
// 全ルートに適用
app.use('*', logger())
app.use('*', cors({ origin: 'http://localhost:3000' }))
// 自作ミドルウェア(レスポンス時間計測)
app.use('*', async (c, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.header('X-Response-Time', `${ms}ms`)
})
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const createUserSchema = z.object({
name: z.string().min(1, '名前は必須です'),
email: z.string().email('メール形式が不正です'),
age: z.number().min(0).max(150).optional(),
})
app.post(
'/users',
zValidator('json', createUserSchema),
async (c) => {
const { name, email, age } = c.req.valid('json')
// ← バリデーション済み・型が自動でつく!
const user = await createUser({ name, email, age })
return c.json({ user }, 201)
}
)
💡 ポイント:
c.req.json()は型なし・バリデーションなし。c.req.valid('json')はバリデーション済み・型付き。必ず後者を使う。
my-app/
├── app/
│ ├── api/
│ │ └── [[...route]]/
│ │ └── route.ts ← Honoの入口(キャッチオールルート)
│ ├── page.tsx
│ └── layout.tsx
├── server/
│ ├── index.ts ← Honoアプリ本体・AppTypeエクスポート
│ └── routes/
│ ├── users.ts
│ ├── posts.ts
│ └── auth.ts
├── lib/
│ ├── supabase.ts ← Supabaseクライアント
│ └── client.ts ← hono/clientセットアップ
└── package.json
// app/api/[[...route]]/route.ts
import { handle } from 'hono/vercel'
import app from '@/server/index'
export const GET = handle(app)
export const POST = handle(app)
export const PUT = handle(app)
export const DELETE = handle(app)
// server/index.ts
import { Hono } from 'hono'
import { usersRoute } from './routes/users'
import { postsRoute } from './routes/posts'
import { authRoute } from './routes/auth'
const app = new Hono().basePath('/api')
.route('/users', usersRoute)
.route('/posts', postsRoute)
.route('/auth', authRoute)
export default app
export type AppType = typeof app // ← hono/clientの型推論に必須!
💡 ポイント:
[[...route]]はNext.jsのキャッチオールルートで/api/以下の全リクエストをHonoに流す。AppTypeのエクスポートを忘れるとhono/clientの型推論が効かなくなる。
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // サーバー側はservice_role_key
)
キーの使い分け(重要)
anon key(公開可)
└── フロント(ブラウザ)で使う
└── RLSが適用される
└── NEXT_PUBLIC_ プレフィックスOK
service_role_key(絶対に公開しない)
└── サーバー(Hono)で使う
└── RLSをバイパスして全データにアクセス
└── NEXT_PUBLIC_ は絶対につけない!
// server/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
import { createClient } from '@supabase/supabase-js'
type Variables = {
userId: string
userEmail: string
}
export const authMiddleware = createMiddleware<{ Variables: Variables }>(
async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: '認証が必要です' }, 401)
}
const token = authHeader.replace('Bearer ', '')
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
const { data: { user }, error } = await supabase.auth.getUser(token)
if (error || !user) {
return c.json({ error: 'トークンが無効です' }, 401)
}
// Djangoの request.user に相当
c.set('userId', user.id)
c.set('userEmail', user.email ?? '')
await next()
}
)
認証ミドルウェアの適用
export const postsRoute = new Hono()
// 認証不要(公開API)
.get('/', async (c) => {
const { data } = await supabase.from('posts').select('*')
return c.json({ posts: data })
})
// 認証必要
.post('/', authMiddleware, zValidator('json', z.object({
title: z.string().min(1),
content: z.string().min(1),
})), async (c) => {
const userId = c.get('userId') // ミドルウェアでセットした値
const body = c.req.valid('json')
const { data, error } = await supabase
.from('posts')
.insert({ ...body, user_id: userId })
.select()
.single()
if (error) return c.json({ error: error.message }, 500)
return c.json({ post: data }, 201)
})
💡 ポイント:
c.set() / c.get()でミドルウェアからハンドラへ値を渡せる。Djangoのrequest.userに相当するイメージ。
// lib/client.ts
import { hc } from 'hono/client'
import type { AppType } from '@/server/index'
export const client = hc<AppType>('/')
Zodスキーマ(server/routes/*.ts)
│ 型が伝播
▼
AppType(server/index.ts)
│ hc<AppType>で渡す
▼
client(lib/client.ts)
│ 補完・型チェックが効く
▼
Next.jsコンポーネント
// app/posts/page.tsx
import { client } from '@/lib/client'
export default async function PostsPage() {
const res = await client.api.posts.$get()
const { posts } = await res.json()
// posts は Post[] 型が自動でつく!
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'use client'
import { client } from '@/lib/client'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
export default function NewPostPage() {
const supabase = createClientComponentClient()
const handleSubmit = async () => {
const { data: { session } } = await supabase.auth.getSession()
const token = session?.access_token
const res = await client.api.posts.$post(
{ json: { title, content } },
{ headers: { Authorization: `Bearer ${token}` } }
)
if (res.ok) {
const { post } = await res.json()
console.log('作成成功:', post.title) // 型付き!
}
}
// ...
}
💡 ポイント: Zodで定義したスキーマの型がそのままフロントまで流れてくる。バックエンドのスキーマ変更時にフロントで型エラーが出るので変更漏れに気づける。APIの型の二重管理が完全に不要になる。
| デプロイ先 | 難易度 | 速度 | コスト | 向いているケース |
|---|---|---|---|---|
| Vercel | ⭐ | 普通 | 中 | 個人開発・新規プロジェクト |
| Cloudflare Workers | ⭐⭐ | 最速 | 安 | 速度・コスト重視 |
| AWS ECS | ⭐⭐⭐ | 普通 | 高 | 既存インフラ・大規模 |
npm i -g vercel
vercel deploy
# 環境変数(Vercelダッシュボードで設定)
NEXT_PUBLIC_SUPABASE_URL = https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY = eyJxxx
SUPABASE_SERVICE_ROLE_KEY = eyJxxx ← NEXT_PUBLIC_なし!
// server/node.ts(ECS用エントリーポイント)
import { serve } from '@hono/node-server'
import app from './index'
serve({ fetch: app.fetch, port: 3001 }, (info) => {
console.log(`Server running on port ${info.port}`)
})
| ミス | 改善策 |
|---|---|
service_role_keyにNEXT_PUBLIC_をつける | サーバー専用キーはNEXT_PUBLIC_なし。ブラウザに漏れると全データ危険 |
c.req.json()を使う | c.req.valid('json')を使う。型なし・バリデーションなしは危険 |
AppTypeをエクスポートし忘れる | hono/clientの型推論が効かなくなる |
| Next.js API Routesと混在させる | [[...route]]でHonoに一本化してルーティングを整理する |
| 最初からECSで構築しようとする | まずVercelで動かす。複雑な構成は後から移行 |
型安全の恩恵が大きい: DjangoではAPIレスポンスの型をフロントに手書きしていたが、Next.js + Honoではそれが完全に不要。スキーマ変更時のフロント修正漏れが防げるのが特に嬉しい。
Djangoとの使い分け: 管理画面・複雑なORM・バッチ処理が必要な部分はDjangoが依然として強い。新規APIはHonoで、既存の複雑なロジックはDjangoのまま、という構成も現実的。
追記予定:
- Drizzle ORM + Supabaseの組み合わせを試したい
- Cloudflare Workers へのマイグレーション手順
- hono/clientとReact Queryの組み合わせ