mcpField
H
hono

hono

github.com/pedronauck開発647292 回インストール1 日前に更新
リポジトリ

hono について

hono は、Hono の Web アプリ構築向け AI スキルです。API の知識を内蔵し、Hono CLI によるアプリの調査とテストを案内します。

主な機能

  1. 01Hono API リファレンスを内蔵
  2. 02CLI によるアプリの調査とテスト
  3. 03サーバー不要のリクエスト・バッチテスト
  4. 04ルートスナップショットによる回帰確認
  5. 05バリデーション、JSX、ストリーミング、RPC のガイド

活用シーン

  1. 01Hono のルートの構築や変更
  2. 02リクエストフローと期待レスポンスのテスト
  3. 03変更前の既存ルート動作の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/pedronauck/skills --skill hono

インストール後、Claude Code で/honoを入力して読み込まれたか確認します。

詳細

Hono スキル

Hono の Web アプリケーションを構築します。このスキルは、AI 向けのインライン API 知識を提供します。Hono CLI を使ってアプリを調査、テストします。

最新のドキュメント

このインラインリファレンスに記載されていない詳細については、https://hono.dev から最新のドキュメントを取得してください。https://hono.dev/llms.txt からドキュメントページの一覧を取得し、Accept: text/markdown ヘッダーを指定してページを取得すると、Markdown 形式で受け取れます。

curl -H "Accept: text/markdown" https://hono.dev/docs/helpers/cookie

Hono CLI

Hono CLI を使ってアプリを調査、テストします。プロジェクトにインストールしてから、CLI に使い方を説明させてください。

npm install -D @hono/cli@next
npx hono agent-context

出力に従ってください。各コマンド(routes、request、batch、snapshot、benchmark、optimize、ssg)、JSON 出力の仕様、ワークフローについて説明されています。

注意事項:

  • hono request は app.request() でリクエストを送信するため、サーバーは不要です。認証情報を CLI 引数に直接渡さないでください。機密性の高い値には環境変数を使用してください。

  • Cloudflare Workers のバインディング(KV、D1、R2 など)には、hono request /path --runtime workerd を使用してください。プロジェクトの wrangler 設定でアプリを起動するため、ローカルのバインディング(c.env)が実際に利用できます。プロジェクトに wrangler をインストールしておく必要があります。

  • 複数のリクエストを実行する場合や、状態を保持するフロー(POST の後に返された ID を使用するなど)には、1 回の hono batch - 呼び出しで実行してください。1 行につき JSON オブジェクトを 1 つ指定し、値を save で保存して、後続のステップで {{id}} として使用します。各ステップは同じアプリインスタンスを共有します。expect でステップごとの期待するステータスと本文を宣言し(本文はディープ部分一致です)、サマリーに "failed": 0 と表示されるまで繰り返してください。

    npx hono batch - <<'EOF'
    {"method":"POST","path":"/users","body":{"name":"Alice"},"save":{"id":".id"},"expect":{"status":201}}
    {"path":"/users/{{id}}","expect":{"status":200,"body":{"name":"Alice"}}}
    EOF
    
  • 既存のルートを変更する前に、現在の動作を記録してください。npx hono snapshot はバッチ JSONL の各行として出力します(実際のレスポンスが expect になります)。行を保存して変更を加えた後、"failed": 0 になるまで npx hono batch - で再実行してください。

  • 大規模な API では、npx hono snapshot --status-only と npx hono batch - --compact を使用してください。同じ手順をより小さな出力で実行できます。失敗したステップには diff が含まれるので、そこに示された内容を修正してください。


Hono API リファレンス

アプリケーションコンストラクター
import { Hono } from 'hono'

const app = new Hono()

// With TypeScript generics
type Env = {
  Bindings: { DATABASE: D1Database; KV: KVNamespace }
  Variables: { user: User }
}
const app = new Hono<Env>()
ルーティングメソッド
app.get('/path', handler)
app.post('/path', handler)
app.put('/path', handler)
app.delete('/path', handler)
app.patch('/path', handler)
app.options('/path', handler)
app.all('/path', handler) // all HTTP methods
app.on('PURGE', '/path', handler) // custom method
app.on(['PUT', 'DELETE'], '/path', handler) // multiple methods
ルーティングパターン
// Path parameters
app.get('/user/:name', (c) => {
  const name = c.req.param('name')
  return c.json({ name })
})

// Multiple params
app.get('/posts/:id/comments/:commentId', (c) => {
  const { id, commentId } = c.req.param()
})

// Optional parameters
app.get('/api/animal/:type?', (c) => c.text('Animal!'))

// Wildcards
app.get('/wild/*/card', (c) => c.text('Wildcard'))

// Regexp constraints
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
  const { date, title } = c.req.param()
})

// Chained routes
app
  .get('/endpoint', (c) => c.text('GET'))
  .post((c) => c.text('POST'))
  .delete((c) => c.text('DELETE'))
ルートのグループ化
// Using route()
const api = new Hono()
api.get('/users', (c) => c.json([]))

const app = new Hono()
app.route('/api', api) // mounts at /api/users

// Using basePath()
const app = new Hono().basePath('/api')
app.get('/users', (c) => c.json([])) // GET /api/users
エラー処理
app.notFound((c) => c.json({ message: 'Not Found' }, 404))

app.onError((err, c) => {
  console.error(err)
  return c.json({ message: 'Internal Server Error' }, 500)
})

コンテキスト(c)

レスポンスメソッド
c.text('Hello') // text/plain
c.json({ message: 'Hello' }) // application/json
c.html('<h1>Hello</h1>') // text/html
c.redirect('/new-path') // 302 redirect
c.redirect('/new-path', 301) // 301 redirect
c.body('raw body', 200, headers) // raw response
c.notFound() // 404 response
ヘッダーとステータス
c.status(201)
c.header('X-Custom', 'value')
c.header('Cache-Control', 'no-store')
変数(リクエスト単位のデータ)
// In middleware
c.set('user', { id: 1, name: 'Alice' })

// In handler
const user = c.get('user')
// or
const user = c.var.user
環境(Cloudflare Workers)
const value = await c.env.KV.get('key')
const db = c.env.DATABASE
c.executionCtx.waitUntil(promise)
レンダラー
app.use(async (c, next) => {
  c.setRenderer((content) =>
    c.html(
      <html><body>{content}</body></html>
    )
  )
  await next()
})

app.get('/', (c) => c.render(<h1>Hello</h1>))

HonoRequest(c.req)

c.req.param('id') // path parameter
c.req.param() // all path params as object
c.req.query('page') // query string parameter
c.req.query() // all query params as object
c.req.queries('tags') // multiple values: ?tags=A&tags=B → ['A', 'B']
c.req.header('Authorization') // request header
c.req.header() // all headers (keys are lowercase)

// Body parsing
await c.req.json() // parse JSON body
await c.req.text() // parse text body
await c.req.formData() // parse as FormData
await c.req.parseBody() // parse multipart/form-data or urlencoded
await c.req.arrayBuffer() // parse as ArrayBuffer
await c.req.blob() // parse as Blob

// Validated data (used with validator middleware)
c.req.valid('json')
c.req.valid('query')
c.req.valid('form')
c.req.valid('param')

// Properties
c.req.url // full URL string
c.req.path // pathname
c.req.method // HTTP method
c.req.raw // underlying Request object

ミドルウェア

組み込みミドルウェアの使用
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { basicAuth } from 'hono/basic-auth'
import { prettyJSON } from 'hono/pretty-json'
import { secureHeaders } from 'hono/secure-headers'
import { etag } from 'hono/etag'
import { compress } from 'hono/compress'
import { poweredBy } from 'hono/powered-by'
import { timing } from 'hono/timing'
import { cache } from 'hono/cache'
import { bearerAuth } from 'hono/bearer-auth'
import { jwt } from 'hono/jwt'
import { jwk } from 'hono/jwk'
import { csrf } from 'hono/csrf'
import { ipRestriction } from 'hono/ip-restriction'
import { bodyLimit } from 'hono/body-limit'
import { timeout } from 'hono/timeout'
import { requestId } from 'hono/request-id'
import { methodOverride } from 'hono/method-override'
import { methodNotAllowed } from 'hono/method-not-allowed'
import { languageDetector } from 'hono/language'
import { some, every, except } from 'hono/combine'
import { contextStorage, getContext } from 'hono/context-storage'
import { trailingSlash, trimTrailingSlash } from 'hono/trailing-slash'

// Registration
app.use(logger()) // all routes
app.use('/api/*', cors()) // specific path
app.post('/api/*', basicAuth({ username: 'admin', password: 'secret' }))
カスタムミドルウェア
// Inline
app.use(async (c, next) => {
  const start = Date.now()
  await next()
  const elapsed = Date.now() - start
  c.res.headers.set('X-Response-Time', `${elapsed}ms`)
})

// Reusable with createMiddleware
import { createMiddleware } from 'hono/factory'

const auth = createMiddleware(async (c, next) => {
  const token = c.req.header('Authorization')
  if (!token) return c.json({ error: 'Unauthorized' }, 401)
  await next()
})

app.use('/api/*', auth)
ミドルウェアの実行順序

ミドルウェアは登録順に実行されます。await next() は次のミドルウェアまたはハンドラーを呼び出し、next() の後のコードは戻り際に実行されます。

Request → mw1 before → mw2 before → handler → mw2 after → mw1 after → Response
app.use(async (c, next) => {
  // before handler
  await next()
  // after handler
})

バリデーション

バリデーション対象:json、form、query、header、param、cookie。

Zod バリデーター
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const schema = z.object({
  title: z.string().min(1),
  body: z.string()
})

app.post('/posts', zValidator('json', schema), (c) => {
  const data = c.req.valid('json') // fully typed
  return c.json(data, 201)
})
Valibot / Standard Schema バリデーター
import { sValidator } from '@hono/standard-validator'
import * as v from 'valibot'

const schema = v.object({ name: v.string(), age: v.number() })

app.post('/users', sValidator('json', schema), (c) => {
  const data = c.req.valid('json')
  return c.json(data, 201)
})

JSX

セットアップ

tsconfig.json で設定します。

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}

または、プラグマを使用します:/** @jsxImportSource hono/jsx */

重要: JSX を使用するファイルの拡張子は .tsx にする必要があります。.ts を .tsx に変更しないと、コンパイラーでエラーが発生します。

コンポーネント
import type { PropsWithChildren } from 'hono/jsx'

const Layout = (props: PropsWithChildren) => (
  <html>
    <head>
      <title>My App</title>
    </head>
    <body>{props.children}</body>
  </html>
)

const UserCard = ({ name }: { name: string }) => (
  <div class="card">
    <h2>{name}</h2>
  </div>
)

app.get('/', (c) => {
  return c.html(
    <Layout>
      <UserCard name="Alice" />
    </Layout>
  )
})
jsxRenderer ミドルウェア

レイアウトには jsxRenderer ミドルウェアを使用してください。詳細については、https://hono.dev/docs/middleware/builtin/jsx-renderer を参照してください。

非同期コンポーネント
const UserList = async () => {
  const users = await fetchUsers()
  return (
    <ul>
      {users.map((u) => (
        <li>{u.name}</li>
      ))}
    </ul>
  )
}
フラグメント
const Items = () => (
  <>
    <li>Item 1</li>
    <li>Item 2</li>
  </>
)

ストリーミング

import { stream, streamText, streamSSE } from 'hono/streaming'

// Basic stream
app.get('/stream', (c) => {
  return stream(c, async (stream) => {
    stream.onAbort(() => console.log('Aborted'))
    await stream.write(new Uint8Array([0x48, 0x65]))
    await stream.pipe(readableStream)
  })
})

// Text stream
app.get('/stream-text', (c) => {
  return streamText(c, async (stream) => {
    await stream.writeln('Hello')
    await stream.sleep(1000)
    await stream.write('World')
  })
})

// Server-Sent Events
app.get('/sse', (c) => {
  return streamSSE(c, async (stream) => {
    let id = 0
    while (true) {
      await stream.writeSSE({
        data: JSON.stringify({ time: new Date().toISOString() }),
        event: 'time-update',
        id: String(id++)
      })
      await stream.sleep(1000)
    }
  })
})

app.request() を使ったテスト

HTTP サーバーを起動せずにエンドポイントをテストします。

// GET
const res = await app.request('/posts')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ posts: [] })

// POST with JSON
const res = await app.request('/posts', {
  method: 'POST',
  body: JSON.stringify({ title: 'Hello' }),
  headers: { 'Content-Type': 'application/json' }
})

// POST with FormData
const formData = new FormData()
formData.append('name', 'Alice')
const res = await app.request('/users', { method: 'POST', body: formData })

// With mock env (Cloudflare Workers bindings)
const res = await app.request('/api/data', {}, { KV: mockKV, DATABASE: mockDB })

// Using Request object
const req = new Request('http://localhost/api', { method: 'DELETE' })
const res = await app.request(req)

Hono Client(RPC)

サーバーとクライアントで型を共有する、型安全な API クライアントです。

重要:型推論を機能させるには、ルートをチェーンする必要があります。チェーンしないと、クライアントはルートの型を推論できません。

// Server: routes MUST be chained to preserve types
const route = app
  .post('/posts', zValidator('json', schema), (c) => {
    return c.json({ ok: true }, 201)
  })
  .get('/posts', (c) => {
    return c.json({ posts: [] })
  })
export type AppType = typeof route

// Client: use hc() with the exported type
import { hc } from 'hono/client'
import type { AppType } from './server'

const client = hc<AppType>('http://localhost:8787/')
const res = await client.posts.$post({ json: { title: 'Hello' } })
const data = await res.json() // fully typed

型ユーティリティ:

import type { InferRequestType, InferResponseType } from 'hono/client'

type ReqType = InferRequestType<typeof client.posts.$post>
type ResType = InferResponseType<typeof client.posts.$post, 200>

ヘルパー

ヘルパーは、hono/<helper-name> からインポートするユーティリティ関数です。

import { getConnInfo } from 'hono/conninfo'
import { getCookie, setCookie, deleteCookie } from 'hono/cookie'
import { css, Style } from 'hono/css'
import { createFactory } from 'hono/factory'
import { html, raw } from 'hono/html'
import { stream, streamText, streamSSE } from 'hono/streaming'
import { testClient } from 'hono/testing'
import { upgradeWebSocket } from 'hono/cloudflare-workers' // or other adapter

利用可能なヘルパー:Accepts、Adapter、ConnInfo、Cookie、css、Dev、Factory、html、JWT、Proxy、Route、SSG、Streaming、Testing、WebSocket。

詳細については、https://hono.dev/docs/helpers/<helper-name> をご覧ください(Accept: text/markdown を指定して取得してください)。

Factory

createFactory を使用すると、Env を一度定義して、アプリ、ミドルウェア、ハンドラー間で共有できます。

import { createFactory } from 'hono/factory'

const factory = createFactory<Env>()

// Create app (Env type is inherited)
const app = factory.createApp()

// Create middleware (Env type is inherited, no need to pass generics)
const mw = factory.createMiddleware(async (c, next) => {
  await next()
})

// Create handlers separately (preserves type inference)
const handlers = factory.createHandlers(logger(), (c) => c.json({ message: 'Hello' }))
app.get('/api', ...handlers)

ベストプラクティス

  • パスパラメーターの型推論を適切に行うため、ルート定義内にハンドラーをインラインで記述します。
  • Rails 風のコントローラーではなく、機能ごとに大規模なアプリを整理するために app.route() を使用します。
  • アプリ、ミドルウェア、ハンドラー間で Env 型を共有するには、createFactory() を使用します。
  • ミドルウェアとハンドラー間でデータを渡すには、c.set()/c.get() を使用します。
  • 複数のリクエスト部分(param + query + json)を検証するには、バリデーターをチェーンします。
  • RPC 用にアプリの型をエクスポートします:export type AppType = typeof routes
  • テストには app.request() を使用します。サーバーを起動する必要はありません。

アダプター

Hono は複数のランタイムで動作します。デフォルトのエクスポートは Cloudflare Workers、Deno、Bun で動作します。Node.js では、Node アダプターを使用します。

// Cloudflare Workers / Deno / Bun
export default app

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

コメント