基本(ルーティング)
Hono のルーティングは Express 風。app.get/post/put/delete/patch でメソッドと path を指定。
path にはパラメータ・ワイルドカード・正規表現が使える。
HTTP メソッド
import { Hono } from "hono"
const app = new Hono()
app.get("/users", (c) => c.json([]))
app.post("/users", async (c) => {
const body = await c.req.json()
return c.json({ created: body }, 201)
})
app.put("/users/:id", (c) => c.json({}))
app.patch("/users/:id", (c) => c.json({}))
app.delete("/users/:id", (c) => c.body(null, 204))
// すべてのメソッド
app.all("/health", (c) => c.text("ok"))
// 任意のメソッドを指定
app.on(["PURGE", "OPTIONS"], "/cache", (c) => c.text(""))
export default app
パスパラメータ
// 単一
app.get("/users/:id", (c) => {
const id = c.req.param("id") // string
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?", (c) => {
const v = c.req.param("version") ?? "v1"
return c.json({ version: v })
})
正規表現でパラメータを制限
// 数字のみ
app.get("/users/:id{[0-9]+}", (c) => { ... })
// UUID
app.get("/users/:id{[0-9a-f-]{36}}", (c) => { ... })
ワイルドカード
// 任意の path
app.get("/files/*", (c) => {
const path = c.req.path // /files/a/b/c
return c.text(path)
})
クエリパラメータ
app.get("/search", (c) => {
const q = c.req.query("q") // 単一
const tags = c.req.queries("tag") // 複数(?tag=a&tag=b)
return c.json({ q, tags })
})
// 全部まとめて
const allQuery = c.req.query() // Record<string, string>
レスポンスヘルパー
| メソッド | 用途 |
|---|---|
c.text(body, status?) | プレーンテキスト |
c.json(obj, status?) | JSON |
c.html(str) | HTML |
c.body(buf, status?, headers?) | 任意のボディ |
c.redirect(url, status?) | リダイレクト(既定 302) |
c.notFound() | 404 |
c.newResponse(body, init) | 低レベル |
app.get("/", (c) => c.text("Hello"))
app.get("/api/data", (c) => c.json({ ok: true }, 200))
app.post("/login", (c) => c.redirect("/dashboard"))
app.get("/admin", (c) => c.body("Forbidden", 403))
ステータスコード・ヘッダー
app.get("/api/data", (c) => {
c.status(200)
c.header("X-Custom", "value")
c.header("Cache-Control", "max-age=60")
return c.json({ ok: true })
})
// または直接 headers を渡す
return c.json({ ok: true }, 200, {
"X-Custom": "value",
})
グルーピング
app.route() でサブアプリを結合できる。コードを分けるのに便利。
// users.ts
import { Hono } from "hono"
const users = new Hono()
users.get("/", (c) => c.json([]))
users.get("/:id", (c) => c.json({ id: c.req.param("id") }))
export default users
// app.ts
import users from "./users"
const app = new Hono()
app.route("/users", users) // /users と /users/:id
ベースパス
const app = new Hono().basePath("/api/v1")
app.get("/users", ...) // /api/v1/users
順序とマッチング
Hono のルートは登録順に評価される。具体的なパスを先に書く。
// 良い順序
app.get("/users/me", meHandler)
app.get("/users/:id", userHandler) // 先に /users/me がマッチ
// 悪い順序
app.get("/users/:id", userHandler) // /users/me も :id にマッチしてしまう
app.get("/users/me", meHandler) // 到達しない
ハンドラのチェーン(Express 風)
1ルートに複数のハンドラを並べられる。前のハンドラで next() を呼ぶと次へ。
app.get(
"/admin",
async (c, next) => { // 1: 認証チェック
if (!c.req.header("Authorization")) {
return c.text("Unauthorized", 401)
}
await next()
},
(c) => c.text("Welcome admin"), // 2: 本処理
)
ミドルウェアの注入
詳しくはミドルウェア。基本は app.use():
import { logger } from "hono/logger"
import { cors } from "hono/cors"
app.use(logger())
app.use("/api/*", cors()) // 特定 path のみ
404 / エラーハンドラ
app.notFound((c) => c.json({ error: "Not Found" }, 404))
app.onError((err, c) => {
console.error(err)
return c.json({ error: err.message }, 500)
})
// 任意の場所で投げて onError で受ける
import { HTTPException } from "hono/http-exception"
app.get("/forbidden", () => {
throw new HTTPException(403, { message: "Forbidden" })
})
JSX レスポンス(HTML を返す)
Hono はJSX を組み込みでサポート。React 互換の構文で SSR できる。
tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}
import { Hono } from "hono"
const app = new Hono()
const Layout = (props: { children: any }) => (
<html>
<head><title>Hono</title></head>
<body>{props.children}</body>
</html>
)
app.get("/", (c) => c.html(
<Layout>
<h1>Hello, JSX!</h1>
</Layout>
))
ストリーミングレスポンス
import { stream, streamText, streamSSE } from "hono/streaming"
app.get("/stream", (c) => {
return streamText(c, async (stream) => {
for (const chunk of ["Hello", " ", "World"]) {
await stream.write(chunk)
await stream.sleep(500)
}
})
})
// Server-Sent Events
app.get("/sse", (c) => {
return streamSSE(c, async (stream) => {
let id = 0
while (true) {
await stream.writeSSE({ id: String(id++), data: `tick ${Date.now()}` })
await stream.sleep(1000)
}
})
})
静的ファイル配信
ランタイムごとにアダプタが違う。Cloudflare では Workers Sites または Pages、Node.js は serveStatic:
// Node.js
import { serveStatic } from "@hono/node-server/serve-static"
app.use("/static/*", serveStatic({ root: "./public" }))
// Cloudflare Pages
import { serveStatic } from "hono/cloudflare-pages"
app.get("/*", serveStatic())
テスト
Hono はfetch を直接受け取るのでテストが簡単:
import { app } from "./app"
const res = await app.request("/users/1")
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ id: "1" })