コンテンツにスキップ

Hono

Cloudflare Workers、Deno、Bun、Vercel、AWS Lambda、Node.jsをサポートするゼロ依存の超高速エッジWebフレームワーク。

コマンド説明
npm create hono@latest my-appnpmで新しいHonoプロジェクトを作成
yarn create hono my-appYarnで作成
pnpm create hono my-apppnpmで作成
bun create hono my-appBunで作成
deno init --lib honoDenoで作成
コマンド説明
npm install hono既存プロジェクトにHonoをインストール
yarn add honoYarnでインストール
pnpm add honopnpmでインストール
bun add honoBunでインストール
# 特定のランタイムテンプレートでプロジェクトを作成
npm create hono@latest my-app -- --template cloudflare-workers
npm create hono@latest my-app -- --template bun
npm create hono@latest my-app -- --template deno
npm create hono@latest my-app -- --template nodejs
npm create hono@latest my-app -- --template vercel
npm create hono@latest my-app -- --template aws-lambda
npm create hono@latest my-app -- --template cloudflare-pages
npm create hono@latest my-app -- --template fastly
コマンド説明
app.get('/path', handler)GETリクエストを処理
app.post('/path', handler)POSTリクエストを処理
app.put('/path', handler)PUTリクエストを処理
app.delete('/path', handler)DELETEリクエストを処理
app.patch('/path', handler)PATCHリクエストを処理
app.options('/path', handler)OPTIONSリクエストを処理
app.all('/path', handler)すべてのHTTPメソッドを処理
app.on('PURGE', '/path', handler)カスタムメソッドを処理
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', status: 'ok' })
})

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

export default app
// パスパラメータ
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('/api/:version?/users', (c) => {
  const version = c.req.param('version') || 'v1'
  return c.json({ version })
})

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

// 正規表現風パターン
app.get('/user/:id{[0-9]+}', (c) => {
  const id = c.req.param('id')
  return c.json({ id: Number(id) })
})
コマンド説明
c.req.query('key')単一のクエリパラメータを取得
c.req.query()すべてのクエリパラメータを取得
c.req.queries('tags')キーに対する値の配列を取得
コマンド説明
c.req.json()JSONボディをパース
c.req.text()ボディをテキストとして取得
c.req.formData()フォームデータをパース
c.req.blob()ボディをBlobとして取得
c.req.arrayBuffer()ボディをArrayBufferとして取得
c.req.parseBody()コンテンツタイプに基づいてボディを自動パース
コマンド説明
c.req.header('Content-Type')リクエストヘッダーを取得
c.req.header()すべてのヘッダーを取得
c.req.methodHTTPメソッドを取得
c.req.url完全なリクエストURLを取得
c.req.pathリクエストパスを取得
c.req.raw生のRequestオブジェクトを取得
c.req.valid('json')バリデーション済みデータを取得
app.post('/users', async (c) => {
  const body = await c.req.json()
  const page = c.req.query('page') || '1'
  const auth = c.req.header('Authorization')

  return c.json({
    user: body,
    page: Number(page),
    authenticated: !!auth
  }, 201)
})
コマンド説明
c.text('Hello')プレーンテキストレスポンス
c.json({ key: 'value' })JSONレスポンス
c.html('<h1>Hi</h1>')HTMLレスポンス
c.redirect('/new-url')302リダイレクト
c.redirect('/new-url', 301)301永続リダイレクト
c.notFound()404レスポンス
c.body(data)生のボディレスポンス
c.newResponse(body, status, headers)完全なカスタムレスポンス

レスポンスヘッダーとストリーミング

Section titled “レスポンスヘッダーとストリーミング”
// ヘッダーを設定
app.get('/api/data', (c) => {
  c.header('X-Custom-Header', 'value')
  c.header('Cache-Control', 'max-age=3600')
  return c.json({ data: 'example' })
})

// ステータスコードを設定
app.post('/items', async (c) => {
  const item = await c.req.json()
  return c.json(item, 201)
})

// ストリームレスポンス
app.get('/stream', (c) => {
  return c.streamText(async (stream) => {
    await stream.write('Hello ')
    await stream.sleep(1000)
    await stream.write('World!')
  })
})
コマンド説明
import { cors } from 'hono/cors'CORSミドルウェア
import { logger } from 'hono/logger'リクエストログ
import { prettyJSON } from 'hono/pretty-json'JSONレスポンスの整形表示
import { basicAuth } from 'hono/basic-auth'Basic認証
import { bearerAuth } from 'hono/bearer-auth'Bearerトークン認証
import { jwt } from 'hono/jwt'JWT認証
import { compress } from 'hono/compress'Gzip/Brotli圧縮
import { etag } from 'hono/etag'ETagキャッシュ
import { secureHeaders } from 'hono/secure-headers'セキュリティヘッダー
import { csrf } from 'hono/csrf'CSRF保護
import { timing } from 'hono/timing'Server-Timingヘッダー
import { cache } from 'hono/cache'キャッシュ制御
import { bodyLimit } from 'hono/body-limit'リクエストボディサイズ制限
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'
import { bearerAuth } from 'hono/bearer-auth'

const app = new Hono()

// グローバルミドルウェア
app.use('*', logger())
app.use('*', secureHeaders())
app.use('*', cors({
  origin: ['https://example.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,
}))

// ルート固有のミドルウェア
app.use('/api/*', bearerAuth({ token: 'my-secret-token' }))

// カスタムミドルウェア
app.use('*', async (c, next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start
  c.header('X-Response-Time', `${duration}ms`)
})
import { jwt, sign } from 'hono/jwt'

const app = new Hono()
const SECRET = 'my-secret-key'

app.use('/api/*', jwt({ secret: SECRET }))

app.post('/login', async (c) => {
  const { email, password } = await c.req.json()
  const token = await sign(
    { email, exp: Math.floor(Date.now() / 1000) + 3600 },
    SECRET
  )
  return c.json({ token })
})

app.get('/api/profile', (c) => {
  const payload = c.get('jwtPayload')
  return c.json({ email: payload.email })
})
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const userSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
})

app.post(
  '/users',
  zValidator('json', userSchema),
  (c) => {
    const user = c.req.valid('json')
    return c.json(user, 201)
  }
)

// クエリパラメータのバリデーション
const querySchema = z.object({
  page: z.string().optional().default('1'),
  limit: z.string().optional().default('10'),
})

app.get(
  '/users',
  zValidator('query', querySchema),
  (c) => {
    const { page, limit } = c.req.valid('query')
    return c.json({ page, limit })
  }
)
const app = new Hono()

// basePathでルートをグループ化
const api = new Hono().basePath('/api')
api.get('/users', (c) => c.json({ users: [] }))
api.post('/users', (c) => c.json({ created: true }))
app.route('/', api)

// バージョン管理されたAPI
const v1 = new Hono()
v1.get('/health', (c) => c.json({ status: 'ok' }))

const v2 = new Hono()
v2.get('/health', (c) => c.json({ status: 'ok', version: 2 }))

app.route('/api/v1', v1)
app.route('/api/v2', v2)
import { HTTPException } from 'hono/http-exception'

// カスタム404
app.notFound((c) => {
  return c.json({ error: 'Not Found' }, 404)
})

// グローバルエラーハンドラー
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse()
  }
  console.error(err)
  return c.json({ error: 'Internal Server Error' }, 500)
})

// HTTP例外をスロー
app.get('/admin', (c) => {
  if (!c.req.header('Authorization')) {
    throw new HTTPException(401, { message: 'Unauthorized' })
  }
  return c.json({ admin: true })
})

ランタイム固有のセットアップ

Section titled “ランタイム固有のセットアップ”
import { Hono } from 'hono'

type Bindings = {
  MY_KV: KVNamespace
  MY_DB: D1Database
}

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

app.get('/kv/:key', async (c) => {
  const value = await c.env.MY_KV.get(c.req.param('key'))
  return c.json({ value })
})

export default app
import { Hono } from 'hono'
import { serve } from '@hono/node-server'

const app = new Hono()
app.get('/', (c) => c.text('Hello Node.js!'))

serve({ fetch: app.fetch, port: 3000 })
import { Hono } from 'hono'

const app = new Hono()
app.get('/', (c) => c.text('Hello Bun!'))

export default { port: 3000, fetch: app.fetch }
import { Hono } from 'npm:hono'

const app = new Hono()
app.get('/', (c) => c.text('Hello Deno!'))

Deno.serve(app.fetch)
// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const app = new Hono()
  .get('/api/users', (c) => {
    return c.json({ users: [{ id: 1, name: 'Alice' }] })
  })
  .post(
    '/api/users',
    zValidator('json', z.object({ name: z.string() })),
    (c) => {
      const { name } = c.req.valid('json')
      return c.json({ id: 2, name }, 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')

// 完全型付き — ルートとレスポンスの自動補完
const res = await client.api.users.$get()
const data = await res.json()
// data: { users: { id: number, name: string }[] }
import { testClient } from 'hono/testing'

const app = new Hono()
  .get('/hello', (c) => c.json({ message: 'Hello!' }))

// testClientを使用(型安全)
const client = testClient(app)
const res = await client.hello.$get()
const body = await res.json()
// body.message === 'Hello!'

// app.request()を直接使用
const res2 = await app.request('/hello')
// res2.status === 200
  1. 環境バインディングにTypeScriptジェネリクスを使用するHono<{ Bindings: MyBindings }>を定義して、環境変数、KVネームスペース、D1データベースの型安全性を得ましょう。

  2. すべての入力をZodでバリデーションする — リクエストボディ、クエリ、パラメータのバリデーションに@hono/zod-validatorを使用しましょう。ランタイムの安全性とTypeScriptの型を一度に得られます。

  3. フロントエンド-バックエンド間にRPCクライアントを使用する — サーバーからAppTypeをエクスポートし、クライアントでhc<AppType>()を使用して、コード生成なしのエンドツーエンドの型安全性を実現しましょう。

  4. パスパターンでミドルウェアを適用する — すべてをグローバルに適用するのではなく、app.use('/api/*', middleware)を使用して特定のルートにミドルウェアをスコープしましょう。

  5. new Hono().basePath()でルートをグループ化する — 関連するエンドポイントを別々のHonoインスタンスに整理し、app.route()でマウントしてクリーンに分離しましょう。

  6. グローバルにエラーを処理するapp.onError()app.notFound()を使用して、すべてのエラーがスタックトレースではなく構造化されたJSONレスポンスを返すようにしましょう。

  7. c.executionCtx.waitUntil()を使用する — Cloudflare Workersでのバックグラウンド処理(アナリティクス、ログ)には、レスポンスをブロックしないようにwaitUntil()を使用しましょう。

  8. ハンドラーを薄く保つ — ビジネスロジックを別の関数/モジュールに抽出しましょう。ルートハンドラーは入力のパース、ロジックの呼び出し、レスポンスの返却のみを行うべきです。

  9. testClientでテストする — Honoの組み込みテストクライアントは型安全で実行中のサーバーを必要としないため、ユニットテストが高速で信頼性があります。

  10. ユーザーがいる場所にデプロイする — Honoはすべての主要なエッジランタイムで動作します。グローバルエッジにはCloudflare Workers、高速処理にはBun、エコシステム互換性にはNode.jsを選びましょう。