From 394b971d9d0922c6b68a63e0985a34d51a1046a5 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:40:17 +0900 Subject: [PATCH 01/29] docs: add the Coding Agents pages --- .vitepress/config.ts | 4 +- docs/agents/cli.md | 223 +++++++++++++++++++++++++++++++++++++++++++ docs/agents/index.md | 61 ++++++++++++ index.md | 2 +- 4 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 docs/agents/cli.md create mode 100644 docs/agents/index.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index bd7ef9bb..21fd4c45 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -231,9 +231,11 @@ const sidebars = (): DefaultTheme.SidebarItem[] => [ ], }, { - text: 'LLM', + text: 'Coding Agents', collapsed: true, items: [ + { text: 'Overview', link: '/docs/agents/' }, + { text: 'Hono CLI', link: '/docs/agents/cli' }, { text: 'Docs List', link: '/llms.txt', diff --git a/docs/agents/cli.md b/docs/agents/cli.md new file mode 100644 index 00000000..57303d75 --- /dev/null +++ b/docs/agents/cli.md @@ -0,0 +1,223 @@ +# Hono CLI + +Hono CLI (`hono`) is a command-line tool for Hono, made for coding agents. It loads your Hono app directly, so an agent can inspect, test, and build the app without starting a server. Commands print JSON by default. `routes`, `request`, `benchmark`, `optimize`, and `ssg` take `--plain` when a human reads the output. + +## Installation + +Install it in your project. Coding agents find it in `package.json`. + +```sh +npm install -D @hono/cli +``` + +Or globally: + +```sh +npm install -g @hono/cli +``` + +## Output + +Commands print JSON to stdout. `snapshot` prints batch JSONL lines instead. + +- Success: `{ "ok": true, "data": ... }` with exit code 0 +- Failure: `{ "ok": false, "error": { "code", "message", "suggestions", "docs" } }` with exit code 1 + +On failure, `error.suggestions` says what to try next, and `error.docs` points to a page on this site. Logs go to stderr. + +`hono --help` starts with a short note for coding agents, and `hono --help` has examples and notes for each command. An agent needs nothing else to use the CLI. + +## Commands + +| Command | What it does | +| ---------------------------- | ------------------------------------------------------ | +| `hono routes [file]` | Show routes of your Hono app | +| `hono request [path] [file]` | Send a request to your app using `app.request()` | +| `hono batch [file]` | Run multiple requests from JSONL using `app.request()` | +| `hono snapshot [file]` | Print the current behavior as batch JSONL lines | +| `hono benchmark [file]` | Measure the performance of your Hono app | +| `hono optimize [entry]` | Build an optimized Hono app | +| `hono ssg [file]` | Generate static files from your Hono app | + +`file` is the path to your app file. When omitted, the app is found in `src/index.ts`, `src/index.tsx`, `src/index.js`, or `src/index.jsx`. TypeScript and JSX are supported. + +### routes + +Show all routes of your Hono app, like [`showRoutes()`](/docs/helpers/dev#showroutes). Routes are resolved from the real app instance, so mounted sub-apps and `basePath` are expanded. + +```sh +hono routes +hono routes --verbose src/app.ts +``` + +- `--verbose` - include middleware +- `--plain` - human-readable output instead of JSON +- `-e, --external ` - mark a package as external (can be used multiple times) + +```json +{ + "ok": true, + "data": { + "router": "SmartRouter + RegExpRouter", + "routes": [ + { + "method": "GET", + "path": "/", + "name": "[handler]", + "isMiddleware": false + }, + { + "method": "POST", + "path": "/posts", + "name": "[handler]", + "isMiddleware": false + } + ] + } +} +``` + +### request + +Send a request to your app through `app.request()`. No server is needed. `path` defaults to `/`. + +```sh +hono request / +hono request /users/123 +hono request /api/users -X POST -d '{"name":"Alice"}' +hono request /api/protected -H 'Authorization: Bearer token' +cat payload.json | hono request /api/users -X POST -d @- +hono request /api/users/123 --trace +hono request / --runtime bun +``` + +- `-X, --method ` - HTTP method (default: `GET`) +- `-d, --data ` - request body (`@file` reads a file, `@-` reads stdin) +- `-H, --header
` - custom header (can be used multiple times) +- `--trace` - include the matched routes in the output +- `--runtime ` - run the app on `node` (default), `bun`, `deno`, or `workerd` +- `--compact` - one-line JSON without the headers +- `--no-bindings` - skip loading the local Cloudflare bindings +- `-w, --watch` - watch for changes and resend the request +- `-o, --output ` - write the response body to a file +- `--plain` - print the raw body like curl (`-i` adds the status and headers, `-I` shows only them) + +A JSON response body is embedded as an object: + +```json +{ + "ok": true, + "data": { + "status": 200, + "headers": { "content-type": "application/json" }, + "body": { "message": "Hello World" } + } +} +``` + +With `--trace`, the output has `matchedRoutes`: which middleware and handler matched, and which one responded. A 404 result suggests running it. + +You can also pass the app code from stdin with `-` as the file. `app` is predefined and exported for you: + +```sh +echo 'app.get("/hello", (c) => c.json({ ok: true }))' | hono request /hello - +``` + +#### Cloudflare bindings + +In a project with a wrangler config, `c.env` carries the real local bindings (KV, D1, R2, vars) automatically, while the app runs on Node.js. This works in `request`, `batch`, and `snapshot`. Skip it with `--no-bindings`. It needs [wrangler](https://developers.cloudflare.com/workers/wrangler/) installed in the project. + +`--runtime workerd` runs the whole app inside workerd instead, with the wrangler config. It is heavier, but it is the full runtime. The entry is `main` in the wrangler config, so pass no file argument. + +### batch + +Run multiple requests from JSONL in one call, in order, against one app instance. In-memory state carries between steps. + +```sh +hono batch - <<'JSONL' +{"path":"/users","expect":{"status":200}} +{"method":"POST","path":"/users","body":{"name":"Momo"},"expect":{"status":201,"body":{"name":"Momo"}},"save":{"id":".id"}} +{"path":"/users/{{id}}","expect":{"status":200}} +{"method":"DELETE","path":"/users/{{id}}","expect":{"status":204}} +JSONL +``` + +One JSON object per line: `method`, `path`, `body`, `headers`, `expect`, and `save`. + +- `expect` declares the acceptance criteria. `status` matches exactly. `body` is a deep partial match: declared fields must match, and extra fields in the response are ignored. +- `save` stores a value from the response body by dot path, and later steps use it as `{{id}}`. +- A step without `expect` passes on any 2xx or 3xx and fails on a 4xx or 5xx. To accept a 4xx on purpose, declare it with `expect.status`. +- A failed step carries `diff`, one line per mismatch. + +The output has the actual `status` and `body` of each step, `pass` per step, and a `summary`. Rerun until `failed` is 0. + +- `-H, --header
` - a shared header for every step +- `--compact` - print only the failed steps and the summary +- `--runtime ` - `node` (default) or `workerd` +- `--no-bindings` - skip loading the local Cloudflare bindings + +### snapshot + +Print the current behavior of the app as batch JSONL lines, to stdout. No file is written. + +```sh +hono snapshot +hono snapshot --status-only src/app.ts +``` + +Paramless GET routes are executed, and their actual status and body become the `expect`. Routes with params and non-GET routes are printed without one, for you to fill in. One probe line records the response for a path that matches no route. + +Capture before a refactor, then rerun the lines with `hono batch` until `failed` is 0. + +- `--status-only` - capture only the status codes, not the bodies +- `--runtime ` - `node` (default) or `workerd` +- `--no-bindings` - skip loading the local Cloudflare bindings + +### benchmark + +Measure the performance of your Hono app. It is a micro benchmark of routing and handlers: `app.request()` is called directly, with no HTTP stack and no network. Each run happens in a fresh process, so results are comparable. + +```sh +hono benchmark +hono benchmark -P /users +hono benchmark -P /users -X POST -d '{"name":"Alice"}' -H 'Content-Type: application/json' +hono benchmark --hono 4.12.3 --hono 4.13.0 +hono benchmark --hono ../hono +``` + +- `-P, --path ` - benchmark only this path (can be used multiple times) +- `-X`, `-d`, `-H` - the method, body, and headers for `-P` paths +- `--duration ` - how long to measure each route (default: `500`) +- `--warmup ` - requests before measuring (default: `30`) +- `--hono ` - benchmark the same app with another Hono: an npm version, or a local checkout + +A few percent of difference is noise. To compare, run it more than once and check that the difference repeats. + +### optimize + +Build an optimized Hono app. The router is replaced with a prepared one, and unused Hono APIs are removed. For a plain bundle, use your normal build tool. This command exists for the Hono-specific optimizations. + +```sh +hono optimize +hono optimize -m -o dist/app.js +``` + +- `-o, --outfile ` - output file (default: `dist/index.js`) +- `-m, --minify` - minify the output +- `-t, --target ` - environment target, such as `node24`, `deno2`, or `es2024` (default: `node20`) +- `--request-body-api-removal ` - `auto` (default), `force`, or `disable`. Request body APIs are removed only when every route method is strictly GET, HEAD, or OPTIONS. +- `--no-hono-api-removal` - keep unused Hono APIs +- `--no-context-response-api-removal` - keep unused response methods of `Context` + +### ssg + +Generate static files from your Hono app, like the [SSG helper](/docs/helpers/ssg). + +```sh +hono ssg +hono ssg -o dist/static src/app.ts +hono ssg --exclude '/api/*' +``` + +- `-o, --outdir ` - output directory (default: `static`) +- `--include ` / `--exclude ` - select routes by path. `*` matches anything diff --git a/docs/agents/index.md b/docs/agents/index.md new file mode 100644 index 00000000..43d51557 --- /dev/null +++ b/docs/agents/index.md @@ -0,0 +1,61 @@ +# Hono for Coding Agents + +Hono is a good fit for coding agents. The API is small and built on Web Standards, so an agent already knows most of it. This page lists what we provide so that agents work well with Hono. + +## Getting started + +Create a project, then run your agent in it. + +```sh +npm create hono@next my-app +``` + +The templates come with what an agent needs: an `AGENTS.md` that tells the agent how to check the app, and [Hono CLI](/docs/agents/cli) as a dev dependency. Nothing else to set up. + +## Documentation for agents + +Every page on this site is available as Markdown. Fetch it with the `Accept: text/markdown` header: + +```sh +curl -H 'Accept: text/markdown' https://hono.dev/docs/api/routing +``` + +To find the right page, start from [`/llms.txt`](/llms.txt). It lists every page with a one-line description. [`/llms-full.txt`](/llms-full.txt) has the whole documentation in one file, and [`/llms-small.txt`](/llms-small.txt) is a shorter version. + +## Hono Skills + +[Hono Skills](https://github.com/honojs/skills) are [Agent Skills](https://agentskills.io) for Hono. The `hono` skill gives an agent an inline API reference and the way to test requests with Hono CLI. The `hono-jsx` skill covers UI with `hono/jsx`. + +```sh +# Claude Code +/plugin marketplace add honojs/skills +/plugin install hono@hono + +# Other agents, via skills.sh +npx skills add honojs/skills +``` + +## Hono CLI + +[Hono CLI](/docs/agents/cli) loads your app directly, so an agent can inspect and test it without starting a server. Every command prints JSON. + +```sh +hono routes # all routes, without reading the source +hono request /users # one request, through app.request() +hono batch - # many requests with expectations, until "failed": 0 +hono snapshot # the current behavior, as batch lines +``` + +`hono --help` starts with a note for the agent, and `hono --help` has examples. No other setup. + +## AGENTS.md + +Agents follow the way your project says to run and verify things. One line in `AGENTS.md` makes them use Hono CLI instead of starting a dev server: + +```md +To check requests, run `npx hono batch - --compact` (heredoc) until the summary shows "failed": 0. +``` + +## How well do agents use Hono? + +We measure it. See [Agent DX](https://agent-dx.hono.dev) for the results. diff --git a/index.md b/index.md index 8be3cc36..db6e0159 100644 --- a/index.md +++ b/index.md @@ -33,5 +33,5 @@ features: details: Hono has built-in middleware, custom middleware, third-party middleware, and helpers. Batteries included. - icon: 😃 title: Delightful DX - details: Super clean APIs. First-class TypeScript support. Now, we've got "Types". + details: Super clean APIs and first-class TypeScript support. Create a project with create-hono and your coding agent is ready to go. --- From 01514ba7c29a637fa3b00643822a68620b9508ed Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:40:17 +0900 Subject: [PATCH 02/29] docs: add the v5 migration guide --- .vitepress/config.ts | 1 + docs/guides/migrating-to-v5.md | 165 +++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 docs/guides/migrating-to-v5.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 21fd4c45..586c702c 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -101,6 +101,7 @@ const sidebars = (): DefaultTheme.SidebarItem[] => [ text: 'Guides', collapsed: true, items: [ + { text: 'Migrating to v5', link: '/docs/guides/migrating-to-v5' }, { text: 'create-hono', link: '/docs/guides/create-hono' }, { text: 'Middleware', link: '/docs/guides/middleware' }, { text: 'Helpers', link: '/docs/guides/helpers' }, diff --git a/docs/guides/migrating-to-v5.md b/docs/guides/migrating-to-v5.md new file mode 100644 index 00000000..782ae544 --- /dev/null +++ b/docs/guides/migrating-to-v5.md @@ -0,0 +1,165 @@ +# Migrating to v5 + +Hono v5 keeps the core small and moves the rest around it. This page lists the breaking changes from v4 and how to update your code. + +## Try the release candidate + +```sh +npm install hono@next +``` + +## ESM only + +`hono` is published as ESM only. The CommonJS build is removed. + +On Node.js, version 22.12 or later is required. `require('hono')` still works there, because Node.js 22.12 can `require()` ES modules. + +## Runtime adapters are separate packages + +The runtime adapters under `hono/` are no longer bundled with `hono`. Install the corresponding package and import from it. + +| Before | After | +| ------------------------- | -------------------------- | +| `hono/aws-lambda` | `@hono/aws-lambda` | +| `hono/bun` | `@hono/bun` | +| `hono/cloudflare-workers` | `@hono/cloudflare-workers` | +| `hono/deno` | `@hono/deno` | +| `hono/lambda-edge` | `@hono/lambda-edge` | +| `hono/netlify` | `@hono/netlify` | +| `hono/service-worker` | `@hono/service-worker` | +| `hono/vercel` | `@hono/vercel` | + +```ts +// Before +import { serveStatic } from 'hono/bun' + +// After +import { serveStatic } from '@hono/bun' +``` + +`hono/cloudflare-pages` is removed without a replacement. Cloudflare recommends Workers with static assets, so use `hono` on Workers instead. + +`hono/adapter` (`env()` and `getRuntimeKey()`) stays in `hono`. + +## Non-Error throws go to `onError` + +A non-Error value thrown from a handler or middleware, such as a string or a plain object, now goes to `onError` wrapped in an `Error`. The original value is available as `err.cause`, and a thrown string is also used as `err.message`. It no longer propagates out of `app.fetch()`. + +## `getColorEnabledAsync()` is removed + +`getColorEnabledAsync()` from `hono/utils/color` is removed. Use `getColorEnabled()` and pass the bindings on Cloudflare Workers. The `logger()` middleware does this by itself, so it needs no change. + +```ts +// From +const enabled = await getColorEnabledAsync() + +// To +const enabled = getColorEnabled(c.env) +``` + +## `c.req.query()` may return `undefined` values + +`c.req.query()` and `c.req.queries()` without a key now return `Record` and `Record`, since a key may be absent. + +```ts +// Before +const { name } = c.req.query() // string + +// After +const { name } = c.req.query() // string | undefined +``` + +## `c.req.json()` returns `unknown` + +`c.req.json()` returns `Promise` instead of `Promise`. Pass the type explicitly, or use `c.req.valid()` with a validator. + +```ts +// Before +const body = await c.req.json() // any + +// After +const body = await c.req.json<{ name: string }>() +``` + +## `c.json()` throws for a value that is not JSON serializable + +`c.json(undefined)` used to return an empty body. It now throws a `TypeError`, like `Response.json()`. The same applies to a function or a symbol. + +## Removed deprecated features + +### `app.mount()` + +Use the [Mount Middleware](/docs/middleware/builtin/mount) instead. + +```ts +import { mount } from 'hono/mount' + +// Before +app.mount('/itty-router', ittyRouter.handle) + +// After +app.all('/itty-router/*', mount(ittyRouter.handle)) +``` + +### `app.fire()` + +Use `fire()` from `@hono/service-worker` instead. + +```ts +import { fire } from '@hono/service-worker' + +// Before +app.fire() + +// After +fire(app) +``` + +### `c.req.matchedRoutes` and `c.req.routePath` + +Use `matchedRoutes()` and `routePath()` from `hono/route` instead. + +```ts +import { routePath } from 'hono/route' + +// Before +app.get('/posts/:id', (c) => c.text(c.req.routePath)) + +// After +app.get('/posts/:id', (c) => c.text(routePath(c))) +``` + +### SSG hook options + +The `beforeRequestHook`, `afterResponseHook`, and `afterGenerateHook` options of `toSSG()` are removed. Pass them as a plugin via `plugins` instead. `defaultPlugin()`, which skips non-200 responses, is applied only when `plugins` is omitted, so add it explicitly if you need it. + +```ts +import { defaultPlugin, toSSG } from 'hono/ssg' + +// Before +toSSG(app, fs, { beforeRequestHook }) + +// After +toSSG(app, fs, { plugins: [{ beforeRequestHook }, defaultPlugin()] }) +``` + +### Others + +- Bearer Auth Middleware - `noAuthenticationHeaderMessage`, `invalidAuthenticationHeaderMessage`, and `invalidTokenMessage` are removed. Use `noAuthenticationHeader.message`, `invalidAuthenticationHeader.message`, and `invalidToken.message` instead. +- Serve Static Middleware - the `pathResolve` option is removed. It was no longer used. +- SSG Helper - `SSG_DISABLED_RESPONSE` is removed. Use `X_HONO_DISABLE_SSG_HEADER_KEY` instead. +- `timingSafeEqual()` in `hono/utils/buffer` only accepts strings. The `hashFunction` option of the Basic Auth and Bearer Auth middleware is typed as `(input: string) => string | null | Promise` accordingly. +- `getQueryStrings()` in `hono/utils/url` is removed. Use the `URL` API instead. +- `UnOfficalStatusCode` in `hono/utils/http-status` is removed. Use `UnofficialStatusCode` instead. + +## `deno.land/x` is no longer published + +This is not a breaking change, but Hono is no longer published to `deno.land/x`. On Deno, use [JSR](/docs/getting-started/deno) instead. + +```ts +// From +import { Hono } from 'https://deno.land/x/hono/mod.ts' + +// To +import { Hono } from 'jsr:@hono/hono' +``` From 93dc52a3a5921147020262f99425267349315144 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:40:17 +0900 Subject: [PATCH 03/29] docs(factory): add defineHandler() and defineMiddleware() --- docs/helpers/factory.md | 299 +++++++++++++++++++++++++++++++++++----- 1 file changed, 261 insertions(+), 38 deletions(-) diff --git a/docs/helpers/factory.md b/docs/helpers/factory.md index a19f2329..f840809d 100644 --- a/docs/helpers/factory.md +++ b/docs/helpers/factory.md @@ -1,57 +1,207 @@ # Factory Helper -The Factory Helper provides useful functions for creating Hono's components such as Middleware. Sometimes it's difficult to set the proper TypeScript types, but this helper facilitates that. +The Factory Helper provides functions for defining Hono's components, such as handlers and middleware, with the proper TypeScript types. ## Import ```ts import { Hono } from 'hono' -import { createFactory, createMiddleware } from 'hono/factory' +import { + createFactory, + defineHandler, + defineMiddleware, +} from 'hono/factory' ``` -## `createFactory()` +## `defineHandler()` -`createFactory()` will create an instance of the Factory class. +`defineHandler()` defines a handler. The value returned from the handler is converted to a Response, so you can return a plain object or a string. ```ts -import { createFactory } from 'hono/factory' +import { defineHandler } from 'hono/factory' -const factory = createFactory() +app.get( + '/ping', + defineHandler(() => ({ pong: true })) +) ``` -You can pass your Env types as Generics: +The returned value is converted as follows: + +| Returned value | Response | +| --------------------- | --------------------- | +| An object | JSON, like `c.json()` | +| A string or JSX | HTML, like `c.html()` | +| `null` or `undefined` | `204 No Content` | +| A `Response` | Returned as is | + +To set the status, call `c.status()` before returning. The status is not included in the types, so return `c.json()` when you want the status in the types for the [RPC](/docs/guides/rpc) client. ```ts -type Env = { - Variables: { - foo: string +app.post( + '/users', + defineHandler((c) => { + c.status(201) + return { id: '1' } + }) +) +``` + +### Middleware + +Pass middleware before the handler. The middleware runs first, and its `Env` flows into the handler, so `c.get()` is typed. + +```ts +const auth = defineMiddleware<{ Variables: { user: User } }>( + async (c, next) => { + c.set('user', await getUser(c)) + await next() } +) + +app.get( + '/me', + defineHandler(auth, (c) => c.get('user')) // `User` +) +``` + +### Validation + +Pass an options object to validate the request before the handler runs. Each option is a [Standard Schema](https://standardschema.dev/), such as a Zod or Valibot schema, for one target of the request: `param`, `query`, `header`, `cookie`, `form`, or `json`. The validated values are passed to the handler as the second argument, and are also available through `c.req.valid()`. + +```ts +import * as z from 'zod' + +app.post( + '/users/:id', + defineHandler({ + param: z.object({ id: z.string() }), + query: z.object({ page: z.coerce.number() }), + json: z.object({ name: z.string() }), + })((c, { param, query, json }) => { + return { id: param.id, page: query.page, name: json.name } + }) +) +``` + +Add `response` to validate the returned value. The value is validated, transformed by the schema, and returned as JSON. With `response`, the handler can return only a value of the schema's input type or a non-JSON `Response` like `c.redirect()`. + +```ts +app.get( + '/users/:id', + defineHandler({ + param: z.object({ id: z.string() }), + response: z.object({ id: z.string(), name: z.string() }), + })(async (c, { param }) => { + return await findUser(param.id) + }) +) +``` + +When the request fails validation, the handler does not run and the response is `400 Bad Request` with the issues of every failed target. The input values are not included. + +```json +{ + "error": "Validation failed", + "issues": [ + { + "slot": "json", + "issues": [ + { + "message": "Invalid input: expected string, received number", + "path": "name" + } + ] + } + ] } +``` -const factory = createFactory() +When the returned value fails validation, the response is `500 Internal Server Error` with `{ "error": "Response validation failed" }` and without the issues. + +Both are thrown as an [`HTTPException`](/docs/api/exception), so you can customize the response in `app.onError()`. The raw issues are in `err.cause.issues`. + +```ts +app.onError((err, c) => { + if (err instanceof HTTPException && err.status === 400) { + console.log(err.cause) // { issues: [{ slot: 'json', issues: [...] }] } + return c.json({ message: 'Bad Request' }, 400) + } + return c.text('Internal Server Error', 500) +}) ``` -### Options +Instead of a schema, you can pass a function. It receives the value and the Context, and the value it returns is used as the validated value. If it returns a `Response`, that response is sent and the handler does not run. -### defaultAppOptions: `HonoOptions` +```ts +app.get( + '/search', + defineHandler({ + query: (value, c) => { + const q = value.q + if (typeof q !== 'string') { + return c.text('q is required', 400) + } + return { q } + }, + })((c, { query }) => ({ results: search(query.q) })) +) +``` -The default options to pass to the Hono application created by `createApp()`. +Middleware can be passed before the handler here too. It runs before the validation. ```ts -const factory = createFactory({ - defaultAppOptions: { strict: false }, +app.post( + '/users', + defineHandler({ json: UserSchema })(auth, (c, { json }) => { + return createUser(c.get('user'), json) + }) +) +``` + +The validated input and the response are inferred by the [RPC](/docs/guides/rpc) client. With `response`, the response type is the schema's output, without the failure responses. + +```ts +const client = hc('http://localhost') + +const res = await client.users[':id'].$post({ + param: { id: '1' }, + json: { name: 'Hono' }, }) +const user = await res.json() // { id: string; name: string } +``` -const app = factory.createApp() // `strict: false` is applied +### Typing the Context + +Without middleware, pass the `Env` and the path as type arguments. + +```ts +type Env = { + Variables: { + user: User + } +} + +app.get( + '/users/:id', + defineHandler((c) => { + c.get('user') // `User` + c.req.param('id') // `string` + return c.get('user') + }) +) ``` -## `createMiddleware()` +To set the `Env` only once, use [`factory.defineHandler()`](#factory-definehandler). + +## `defineMiddleware()` -`createMiddleware()` is shortcut of `factory.createMiddleware()`. -This function will create your custom middleware. +`defineMiddleware()` defines a middleware with the types. ```ts -const messageMiddleware = createMiddleware(async (c, next) => { +import { defineMiddleware } from 'hono/factory' + +const messageMiddleware = defineMiddleware(async (c, next) => { await next() c.res.headers.set('X-Message', 'Good morning!') }) @@ -61,7 +211,7 @@ Tip: If you want to get an argument like `message`, you can create it as a funct ```ts const messageMiddleware = (message: string) => { - return createMiddleware(async (c, next) => { + return defineMiddleware(async (c, next) => { await next() c.res.headers.set('X-Message', message) }) @@ -70,28 +220,67 @@ const messageMiddleware = (message: string) => { app.use(messageMiddleware('Good evening!')) ``` -## `factory.createHandlers()` +## `createFactory()` -`createHandlers()` helps to define handlers in a different place than `app.get('/')`. +`createFactory()` will create an instance of the Factory class. ```ts import { createFactory } from 'hono/factory' -import { logger } from 'hono/logger' - -// ... const factory = createFactory() +``` -const middleware = factory.createMiddleware(async (c, next) => { - c.set('foo', 'bar') - await next() +You can pass your Env types as Generics: + +```ts +type Env = { + Variables: { + foo: string + } +} + +const factory = createFactory() +``` + +### Options + +### defaultAppOptions: `HonoOptions` + +The default options to pass to the Hono application created by `createApp()`. + +```ts +const factory = createFactory({ + defaultAppOptions: { strict: false }, }) -const handlers = factory.createHandlers(logger(), middleware, (c) => { - return c.json(c.var.foo) +const app = factory.createApp() // `strict: false` is applied +``` + +## `factory.defineHandler()` + +`factory.defineHandler()` is `defineHandler()` with the `Env` of the factory. Everything in [`defineHandler()`](#definehandler) applies. + +```ts +const factory = createFactory() + +const getUser = factory.defineHandler((c) => { + return c.get('foo') // `string` }) -app.get('/api', ...handlers) +app.get('/users/:id', getUser) +``` + +## `factory.defineMiddleware()` + +`factory.defineMiddleware()` is `defineMiddleware()` with the `Env` of the factory. + +```ts +const factory = createFactory() + +const middleware = factory.defineMiddleware(async (c, next) => { + c.set('foo', 'bar') + await next() +}) ``` ## `factory.createApp()` @@ -101,7 +290,7 @@ app.get('/api', ...handlers) If your application is like this, you have to set the `Env` in two places: ```ts -import { createMiddleware } from 'hono/factory' +import { defineMiddleware } from 'hono/factory' type Env = { Variables: { @@ -112,8 +301,8 @@ type Env = { // 1. Set the `Env` to `new Hono()` const app = new Hono() -// 2. Set the `Env` to `createMiddleware()` -const mw = createMiddleware(async (c, next) => { +// 2. Set the `Env` to `defineMiddleware()` +const mw = defineMiddleware(async (c, next) => { await next() }) @@ -132,8 +321,8 @@ const factory = createFactory() const app = factory.createApp() -// factory also has `createMiddleware()` -const mw = factory.createMiddleware(async (c, next) => { +// factory also has `defineMiddleware()` +const mw = factory.defineMiddleware(async (c, next) => { await next() }) ``` @@ -173,3 +362,37 @@ app.post('/posts', (c) => { // ... }) ``` + +## `createMiddleware()` + +**`createMiddleware()` is deprecated**. Use [`defineMiddleware()`](#definemiddleware) instead. It is the same function with a new name, and `factory.createMiddleware()` is `factory.defineMiddleware()`. + +```ts +// Before +const mw = createMiddleware(async (c, next) => { + await next() +}) + +// After +const mw = defineMiddleware(async (c, next) => { + await next() +}) +``` + +## `factory.createHandlers()` + +**`factory.createHandlers()` is deprecated**. Use [`defineHandler()`](#definehandler) instead, and pass the middleware before the handler. + +```ts +// Before +const handlers = factory.createHandlers(logger(), middleware, (c) => { + return c.json(c.var.foo) +}) +app.get('/api', ...handlers) + +// After +const handler = factory.defineHandler(logger(), middleware, (c) => { + return c.json(c.var.foo) +}) +app.get('/api', handler) +``` From 011a1dd3fe94d1f522abe477fff51ce559733c2c Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:40:17 +0900 Subject: [PATCH 04/29] docs: rewrite Best Practices with defineHandler() --- docs/guides/best-practices.md | 231 +++++++++++++++++++++++++++------- 1 file changed, 185 insertions(+), 46 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index cccd045a..0deae199 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -3,81 +3,150 @@ Hono is very flexible. You can write your app as you like. However, there are best practices that are better to follow. -## Don't make "Controllers" when possible +## Define handlers with `defineHandler()` -When possible, you should not create "Ruby on Rails-like Controllers". +A handler written as a plain function loses the types. The path parameter cannot be inferred, and the `Env` is unknown. ```ts // 🙁 -// A RoR-like Controller -const booksList = (c: Context) => { - return c.json('list books') -} - -app.get('/books', booksList) -``` - -The issue is related to types. For example, the path parameter cannot be inferred in the Controller without writing complex generics. - -```ts -// 🙁 -// A RoR-like Controller const bookPermalink = (c: Context) => { const id = c.req.param('id') // Can't infer the path param return c.json(`get ${id}`) } + +app.get('/books/:id', bookPermalink) ``` -Therefore, you don't need to create RoR-like controllers and should write handlers directly after path definitions. +Define it with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory` instead. Pass the `Env` and the path as type arguments, and the handler is typed wherever you write it. The returned value is converted to a Response, so you can return a plain object. ```ts +import { defineHandler } from 'hono/factory' + // 😃 -app.get('/books/:id', (c) => { +const bookPermalink = defineHandler((c) => { const id = c.req.param('id') // Can infer the path param - return c.json(`get ${id}`) + return { id } }) + +app.get('/books/:id', bookPermalink) +``` + +Writing the handler inline is fine too. The path is inferred from `app.get()`, and the return value is still converted. + +```ts +// 😃 +app.get( + '/books/:id', + defineHandler((c) => { + const id = c.req.param('id') + return { id } + }) +) ``` -## `factory.createHandlers()` in `hono/factory` +## Validate in `defineHandler()` -If you still want to create a RoR-like Controller, use `factory.createHandlers()` in [`hono/factory`](/docs/helpers/factory). If you use this, type inference will work correctly. +Put the validation in `defineHandler()` instead of a validator middleware. Pass a [Standard Schema](https://standardschema.dev/), such as a Zod schema, for each target of the request. The validated values come as the second argument with the types, and the request is rejected with `400 Bad Request` before the handler runs. ```ts -import { createFactory } from 'hono/factory' -import { logger } from 'hono/logger' +import * as z from 'zod' + +// 😃 +const createBook = defineHandler({ + json: z.object({ title: z.string(), author: z.string() }), +})(async (c, { json }) => { + const book = await db.books.create(json) // `{ title: string; author: string }` + c.status(201) + return book +}) -// ... +app.post('/books', createBook) +``` + +With `param`, the path parameter is typed without the path type argument. +```ts // 😃 -const factory = createFactory() +const bookPermalink = defineHandler({ + param: z.object({ id: z.string() }), +})(async (c, { param }) => { + return await db.books.find(param.id) +}) -const middleware = factory.createMiddleware(async (c, next) => { - c.set('foo', 'bar') - await next() +app.get('/books/:id', bookPermalink) +``` + +Add `response` when the shape of the response matters, for example when it is a public API. The returned value is validated, and extra fields are stripped by the schema. + +```ts +const BookSchema = z.object({ id: z.string(), title: z.string() }) + +// 😃 +const bookPermalink = defineHandler({ + param: z.object({ id: z.string() }), + response: BookSchema, +})(async (c, { param }) => { + return await db.books.find(param.id) // Only `id` and `title` are sent }) +``` + +## Put middleware next to the handler + +Middleware that belongs to one handler, such as authentication, goes before the handler in `defineHandler()`. The `Env` of the middleware flows into the handler, so `c.get()` is typed. Middleware for many routes still goes in `app.use()`. + +```ts +import { defineHandler, defineMiddleware } from 'hono/factory' + +const auth = defineMiddleware<{ Variables: { user: User } }>( + async (c, next) => { + c.set('user', await getUser(c)) + await next() + } +) -const handlers = factory.createHandlers(logger(), middleware, (c) => { - return c.json(c.var.foo) +// 😃 +const createBook = defineHandler({ + json: z.object({ title: z.string() }), +})(auth, async (c, { json }) => { + return await db.books.create({ ...json, owner: c.get('user').id }) }) -app.get('/api', ...handlers) +app.post('/books', createBook) ``` ## Building a larger application -Use `app.route()` to build a larger application without creating "Ruby on Rails-like Controllers". +Use `app.route()` to build a larger application. Each resource gets its own file with a Hono instance and its handlers. If your application has `/authors` and `/books` endpoints and you wish to separate files from `index.ts`, create `authors.ts` and `books.ts`. ```ts // authors.ts import { Hono } from 'hono' +import { defineHandler } from 'hono/factory' +import * as z from 'zod' const app = new Hono() -app.get('/', (c) => c.json('list authors')) -app.post('/', (c) => c.json('create an author', 201)) -app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`)) +app.get( + '/', + defineHandler(async () => await db.authors.list()) +) +app.post( + '/', + defineHandler({ json: z.object({ name: z.string() }) })( + async (c, { json }) => { + c.status(201) + return await db.authors.create(json) + } + ) +) +app.get( + '/:id', + defineHandler({ param: z.object({ id: z.string() }) })( + async (c, { param }) => await db.authors.find(param.id) + ) +) export default app ``` @@ -85,12 +154,30 @@ export default app ```ts // books.ts import { Hono } from 'hono' +import { defineHandler } from 'hono/factory' +import * as z from 'zod' const app = new Hono() -app.get('/', (c) => c.json('list books')) -app.post('/', (c) => c.json('create a book', 201)) -app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`)) +app.get( + '/', + defineHandler(async () => await db.books.list()) +) +app.post( + '/', + defineHandler({ json: z.object({ title: z.string() }) })( + async (c, { json }) => { + c.status(201) + return await db.books.create(json) + } + ) +) +app.get( + '/:id', + defineHandler({ param: z.object({ id: z.string() }) })( + async (c, { param }) => await db.books.find(param.id) + ) +) export default app ``` @@ -112,19 +199,65 @@ app.route('/books', books) export default app ``` +When the handlers grow, move them out of the route file. A handler defined with `defineHandler()` keeps its types in any file. + +```ts +// books/handlers.ts +export const listBooks = defineHandler( + async () => await db.books.list() +) + +export const createBook = defineHandler({ + json: z.object({ title: z.string() }), +})(async (c, { json }) => { + c.status(201) + return await db.books.create(json) +}) +``` + +```ts +// books/index.ts +import { createBook, listBooks } from './handlers' + +const app = new Hono() + +app.get('/', listBooks) +app.post('/', createBook) + +export default app +``` + ### If you want to use RPC features The code above works well for normal use cases. -However, if you want to use the `RPC` feature, you can get the correct type by chaining as follows. +However, if you want to use the `RPC` feature, you can get the correct type by chaining as follows. `defineHandler()` gives the client the validated input and the returned value. ```ts // authors.ts import { Hono } from 'hono' +import { defineHandler } from 'hono/factory' +import * as z from 'zod' const app = new Hono() - .get('/', (c) => c.json('list authors')) - .post('/', (c) => c.json('create an author', 201)) - .get('/:id', (c) => c.json(`get ${c.req.param('id')}`)) + .get( + '/', + defineHandler(async () => await db.authors.list()) + ) + .post( + '/', + defineHandler({ json: z.object({ name: z.string() }) })( + async (c, { json }) => { + c.status(201) + return await db.authors.create(json) + } + ) + ) + .get( + '/:id', + defineHandler({ param: z.object({ id: z.string() }) })( + async (c, { param }) => await db.authors.find(param.id) + ) + ) export default app export type AppType = typeof app @@ -138,6 +271,9 @@ import { hc } from 'hono/client' // 😃 const client = hc('http://localhost') // Typed correctly + +const res = await client.index.$post({ json: { name: 'Yusuke' } }) +const author = await res.json() // The type of `db.authors.create()` ``` For more detailed information, please see [the RPC page](/docs/guides/rpc#using-rpc-with-larger-applications). @@ -152,11 +288,14 @@ Hono automatically handles HEAD requests by converting them to GET requests and ```typescript // GOOD: This GET route automatically handles HEAD requests -app.get('/api/users', async (c) => { - const users = await getUsers() - c.header('X-Total-Count', users.length.toString()) - return c.json(users) -}) +app.get( + '/api/users', + defineHandler(async (c) => { + const users = await getUsers() + c.header('X-Total-Count', users.length.toString()) + return users + }) +) // HEAD /api/users will return: // - Same headers as GET (including X-Total-Count) From 7cf07f92f00df0fbe21aa06a83b4c2360d9ad13c Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:45:55 +0900 Subject: [PATCH 05/29] docs: mention coding agents in Getting Started --- docs/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index b03f38dd..2adaae91 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,13 +47,15 @@ deno init --npm hono@latest ::: +The project comes with an `AGENTS.md` and [Hono CLI](/docs/agents/cli), so your coding agent is ready to go. See [Hono for Coding Agents](/docs/agents/). + ## Features - **Ultrafast** 🚀 - The router `RegExpRouter` is really fast. Not using linear loops. Fast. - **Lightweight** ðŸŠķ - The `hono/tiny` preset is under 14kB. Hono has zero dependencies and uses only the Web Standards. - **Multi-runtime** 🌍 - Works on Cloudflare Workers, Fastly Compute, Deno, Bun, AWS Lambda, or Node.js. The same code runs on all platforms. - **Batteries Included** 🔋 - Hono has built-in middleware, custom middleware, third-party middleware, and helpers. Batteries included. -- **Delightful DX** 😃 - Super clean APIs. First-class TypeScript support. Now, we've got "Types". +- **Delightful DX** 😃 - Super clean APIs and first-class TypeScript support. Create a project with create-hono and your coding agent is ready to go. ## Use-cases From 1ab3c8484b5adc6160da403e9f3a965d7a9672cf Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:45:55 +0900 Subject: [PATCH 06/29] feat: show the create command on the top page --- .vitepress/theme/Layout.vue | 4 ++ .vitepress/theme/components/HeroCommand.vue | 67 +++++++++++++++++++++ .vitepress/theme/custom.css | 52 ++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 .vitepress/theme/components/HeroCommand.vue diff --git a/.vitepress/theme/Layout.vue b/.vitepress/theme/Layout.vue index 10bb5bb4..beb1cf0a 100644 --- a/.vitepress/theme/Layout.vue +++ b/.vitepress/theme/Layout.vue @@ -1,5 +1,6 @@ + + diff --git a/.vitepress/theme/custom.css b/.vitepress/theme/custom.css index 32707927..7a822505 100644 --- a/.vitepress/theme/custom.css +++ b/.vitepress/theme/custom.css @@ -330,3 +330,55 @@ html.kawaii-mode .hero-image-slot { display: none; } } + +/* The create command under the hero buttons. Follows the hero's text-align, so it is + centered on narrow screens and left-aligned next to the code window. */ +.hero-command { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 20px; + padding: 0 6px 0 16px; + height: 40px; + border: 1px solid var(--vp-c-divider); + border-radius: 20px; + background-color: var(--vp-c-bg-soft); +} + +.hero-command-text { + font-family: var(--vp-font-family-mono); + font-size: 14px; + color: var(--vp-c-text-1); + white-space: nowrap; +} + +.hero-command-prompt { + color: var(--vp-c-text-3); + user-select: none; +} + +.hero-command-copy { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 14px; + color: var(--vp-c-text-2); + transition: color 0.25s, background-color 0.25s; +} + +.hero-command-copy:hover { + color: var(--vp-c-text-1); + background-color: var(--vp-c-default-soft); +} + +.hero-command-copy.copied { + color: var(--vp-c-brand-1); +} + +@media (min-width: 640px) { + .hero-command { + margin-top: 24px; + } +} From 71ab6c7b22fc0b99f2702c408a9f2fe385cbd193 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:51:12 +0900 Subject: [PATCH 07/29] docs(agents): install the skills with skills.sh or GitHub CLI --- docs/agents/index.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/agents/index.md b/docs/agents/index.md index 43d51557..5a917893 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -26,13 +26,15 @@ To find the right page, start from [`/llms.txt`](/llms.txt). It lists every page [Hono Skills](https://github.com/honojs/skills) are [Agent Skills](https://agentskills.io) for Hono. The `hono` skill gives an agent an inline API reference and the way to test requests with Hono CLI. The `hono-jsx` skill covers UI with `hono/jsx`. -```sh -# Claude Code -/plugin marketplace add honojs/skills -/plugin install hono@hono +Install them with [skills.sh](https://skills.sh) or [GitHub CLI](https://cli.github.com). Both put the skills where your agent reads them. -# Other agents, via skills.sh +```sh +# skills.sh npx skills add honojs/skills + +# GitHub CLI +gh skill install honojs/skills hono +gh skill install honojs/skills hono-jsx ``` ## Hono CLI From 0ae7a7875911ae5a1091601d64b708b48c04554e Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:52:59 +0900 Subject: [PATCH 08/29] docs(agents): show the Verify section for AGENTS.md --- docs/agents/index.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/agents/index.md b/docs/agents/index.md index 5a917893..1fcccb30 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -52,10 +52,18 @@ hono snapshot # the current behavior, as batch lines ## AGENTS.md -Agents follow the way your project says to run and verify things. One line in `AGENTS.md` makes them use Hono CLI instead of starting a dev server: +Agents follow the way your project says to run and verify things. A few lines in `AGENTS.md` make them use Hono CLI instead of a dev server. The create-hono templates already have them. ```md -To check requests, run `npx hono batch - --compact` (heredoc) until the summary shows "failed": 0. +## Verify + +Verify with the Hono CLI, not with a dev server. +It loads the app in-process and prints JSON. + +- `npx hono routes` lists the routes. +- `npx hono request /` sends one request. +- To check requests, run `npx hono batch - --compact` (heredoc) + until the summary shows "failed": 0. ``` ## How well do agents use Hono? From 4b948e05c39b793055b90d47ef56333a5571ec97 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 18:54:05 +0900 Subject: [PATCH 09/29] docs(agents): point AGENTS.md at hono --help --- docs/agents/index.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/agents/index.md b/docs/agents/index.md index 1fcccb30..ee193057 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -52,18 +52,13 @@ hono snapshot # the current behavior, as batch lines ## AGENTS.md -Agents follow the way your project says to run and verify things. A few lines in `AGENTS.md` make them use Hono CLI instead of a dev server. The create-hono templates already have them. +Agents follow the way your project says to run and verify things. Two lines in `AGENTS.md` are enough to make them use Hono CLI instead of a dev server. The agent learns the rest from `hono --help`. The create-hono templates already have them. ```md ## Verify Verify with the Hono CLI, not with a dev server. -It loads the app in-process and prints JSON. - -- `npx hono routes` lists the routes. -- `npx hono request /` sends one request. -- To check requests, run `npx hono batch - --compact` (heredoc) - until the summary shows "failed": 0. +Run `npx hono --help` first. It tells you how. ``` ## How well do agents use Hono? From e6e33b673eec225d6f0f726f2bc85adb6a03821a Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:04:07 +0900 Subject: [PATCH 10/29] docs(agents): the Verify lines also work as a prompt --- docs/agents/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/agents/index.md b/docs/agents/index.md index ee193057..93cd35b4 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -61,6 +61,12 @@ Verify with the Hono CLI, not with a dev server. Run `npx hono --help` first. It tells you how. ``` +Or say it to the agent directly. The same two lines work as a prompt: + +```txt +Verify with the Hono CLI, not with a dev server. Run `npx hono --help` first. +``` + ## How well do agents use Hono? We measure it. See [Agent DX](https://agent-dx.hono.dev) for the results. From f173a1bc37b53b9108de3094d1fb1ebe8cdb4038 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:11:54 +0900 Subject: [PATCH 11/29] docs(agents): one line for AGENTS.md, under a Hono CLI heading --- docs/agents/index.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/agents/index.md b/docs/agents/index.md index 93cd35b4..fae7e0ca 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -52,20 +52,15 @@ hono snapshot # the current behavior, as batch lines ## AGENTS.md -Agents follow the way your project says to run and verify things. Two lines in `AGENTS.md` are enough to make them use Hono CLI instead of a dev server. The agent learns the rest from `hono --help`. The create-hono templates already have them. +Agents follow the way your project says to run and check things. One line in `AGENTS.md` is enough to make them use Hono CLI instead of a dev server. The agent learns the rest from `hono --help`. The create-hono templates already have it. ```md -## Verify +## Hono CLI -Verify with the Hono CLI, not with a dev server. -Run `npx hono --help` first. It tells you how. +Run `npx hono --help` first, and use the CLI instead of a dev server. ``` -Or say it to the agent directly. The same two lines work as a prompt: - -```txt -Verify with the Hono CLI, not with a dev server. Run `npx hono --help` first. -``` +Or say it to the agent directly. The same line works as a prompt. ## How well do agents use Hono? From 109c16ffdff6b716a10f9af58313b397bf0558f7 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:24:08 +0900 Subject: [PATCH 12/29] docs: drop the type arguments from Best Practices --- docs/guides/best-practices.md | 49 ++++++++++++++++------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index 0deae199..7a91c11c 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -17,33 +17,41 @@ const bookPermalink = (c: Context) => { app.get('/books/:id', bookPermalink) ``` -Define it with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory` instead. Pass the `Env` and the path as type arguments, and the handler is typed wherever you write it. The returned value is converted to a Response, so you can return a plain object. +Write it inline with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. The path is inferred from `app.get()`, and the returned value is converted to a Response, so you can return a plain object. ```ts import { defineHandler } from 'hono/factory' -// 😃 -const bookPermalink = defineHandler((c) => { - const id = c.req.param('id') // Can infer the path param - return { id } -}) - -app.get('/books/:id', bookPermalink) -``` - -Writing the handler inline is fine too. The path is inferred from `app.get()`, and the return value is still converted. - -```ts // 😃 app.get( '/books/:id', defineHandler((c) => { - const id = c.req.param('id') + const id = c.req.param('id') // Can infer the path param return { id } }) ) ``` +To define the handler somewhere else, give it what it needs instead of the path. Validate `param`, and the parameter is typed in any file. Create the handler from a factory, and it has the `Env`. + +```ts +import { createFactory } from 'hono/factory' +import * as z from 'zod' + +const factory = createFactory() + +// 😃 +const bookPermalink = factory.defineHandler({ + param: z.object({ id: z.string() }), +})(async (c, { param }) => { + return await db.books.find(param.id) // `param.id` is `string` +}) + +app.get('/books/:id', bookPermalink) +``` + +If you need the types without validation or a factory, `defineHandler()` also takes the `Env` and the path as type arguments. See [Typing the Context](/docs/helpers/factory#typing-the-context). + ## Validate in `defineHandler()` Put the validation in `defineHandler()` instead of a validator middleware. Pass a [Standard Schema](https://standardschema.dev/), such as a Zod schema, for each target of the request. The validated values come as the second argument with the types, and the request is rejected with `400 Bad Request` before the handler runs. @@ -63,19 +71,6 @@ const createBook = defineHandler({ app.post('/books', createBook) ``` -With `param`, the path parameter is typed without the path type argument. - -```ts -// 😃 -const bookPermalink = defineHandler({ - param: z.object({ id: z.string() }), -})(async (c, { param }) => { - return await db.books.find(param.id) -}) - -app.get('/books/:id', bookPermalink) -``` - Add `response` when the shape of the response matters, for example when it is a public API. The returned value is validated, and extra fields are stripped by the schema. ```ts From b4cf2db6ee0e9956a648ab15db731d9d171668eb Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:26:51 +0900 Subject: [PATCH 13/29] docs: Env as a type argument, factory for larger apps --- docs/guides/best-practices.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index 7a91c11c..53b9104a 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -32,16 +32,22 @@ app.get( ) ``` -To define the handler somewhere else, give it what it needs instead of the path. Validate `param`, and the parameter is typed in any file. Create the handler from a factory, and it has the `Env`. +To define the handler somewhere else, pass the `Env` as a type argument when the handler needs it. ```ts -import { createFactory } from 'hono/factory' -import * as z from 'zod' +// 😃 +const me = defineHandler((c) => c.get('user')) + +app.get('/me', me) +``` -const factory = createFactory() +For the path parameter, validate `param`. The parameter is typed in any file. + +```ts +import * as z from 'zod' // 😃 -const bookPermalink = factory.defineHandler({ +const bookPermalink = defineHandler({ param: z.object({ id: z.string() }), })(async (c, { param }) => { return await db.books.find(param.id) // `param.id` is `string` @@ -50,7 +56,7 @@ const bookPermalink = factory.defineHandler({ app.get('/books/:id', bookPermalink) ``` -If you need the types without validation or a factory, `defineHandler()` also takes the `Env` and the path as type arguments. See [Typing the Context](/docs/helpers/factory#typing-the-context). +Pass the path as the second type argument, `defineHandler`, only when you need the path type without validation. In a large application where many handlers share the `Env`, create them from a factory instead of writing the `Env` each time. See [`factory.defineHandler()`](/docs/helpers/factory#factory-definehandler). ## Validate in `defineHandler()` From 89bb48544617272d56405645b5db70969b66c0dd Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:28:22 +0900 Subject: [PATCH 14/29] docs: plain handlers also lose the RPC types --- docs/guides/best-practices.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index 53b9104a..a2d2e1fc 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -5,7 +5,7 @@ However, there are best practices that are better to follow. ## Define handlers with `defineHandler()` -A handler written as a plain function loses the types. The path parameter cannot be inferred, and the `Env` is unknown. +A handler written as a plain function loses the types. The path parameter cannot be inferred, the `Env` is unknown, and the response type does not reach the [RPC](/docs/guides/rpc) client. ```ts // 🙁 @@ -17,7 +17,7 @@ const bookPermalink = (c: Context) => { app.get('/books/:id', bookPermalink) ``` -Write it inline with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. The path is inferred from `app.get()`, and the returned value is converted to a Response, so you can return a plain object. +Write it inline with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. The path is inferred from `app.get()`. The returned value is converted to a Response, so you can return a plain object, and its type reaches the RPC client. ```ts import { defineHandler } from 'hono/factory' From f106dfcbe80928ba28423414ea8cd90667a73de3 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:30:05 +0900 Subject: [PATCH 15/29] docs: a handler defined elsewhere needs no type argument --- docs/guides/best-practices.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index a2d2e1fc..d08edb13 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -32,7 +32,16 @@ app.get( ) ``` -To define the handler somewhere else, pass the `Env` as a type argument when the handler needs it. +Defining the handler somewhere else is fine too. The types stay, and no type argument is needed. + +```ts +// 😃 +const listBooks = defineHandler(async () => await db.books.list()) + +app.get('/books', listBooks) +``` + +Pass the `Env` as a type argument when the handler needs it. ```ts // 😃 @@ -41,7 +50,7 @@ const me = defineHandler((c) => c.get('user')) app.get('/me', me) ``` -For the path parameter, validate `param`. The parameter is typed in any file. +The path is not known there, so validate `param` to type the parameter. ```ts import * as z from 'zod' From 71e39e7baad2f8f1b89ad7763a5b8eb5d2701665 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:32:42 +0900 Subject: [PATCH 16/29] docs: compare a handler variable, not an inline handler --- docs/guides/best-practices.md | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index d08edb13..5661e558 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -5,7 +5,7 @@ However, there are best practices that are better to follow. ## Define handlers with `defineHandler()` -A handler written as a plain function loses the types. The path parameter cannot be inferred, the `Env` is unknown, and the response type does not reach the [RPC](/docs/guides/rpc) client. +A handler written inline is typed by `app.get()`. Once you move it to a variable, a plain function loses the types. The `Env` is unknown, and the path parameter cannot be inferred. ```ts // 🙁 @@ -17,24 +17,11 @@ const bookPermalink = (c: Context) => { app.get('/books/:id', bookPermalink) ``` -Write it inline with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. The path is inferred from `app.get()`. The returned value is converted to a Response, so you can return a plain object, and its type reaches the RPC client. +Define it with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. The types stay, and the returned value is converted to a Response, so you can return a plain object. ```ts import { defineHandler } from 'hono/factory' -// 😃 -app.get( - '/books/:id', - defineHandler((c) => { - const id = c.req.param('id') // Can infer the path param - return { id } - }) -) -``` - -Defining the handler somewhere else is fine too. The types stay, and no type argument is needed. - -```ts // 😃 const listBooks = defineHandler(async () => await db.books.list()) @@ -67,6 +54,8 @@ app.get('/books/:id', bookPermalink) Pass the path as the second type argument, `defineHandler`, only when you need the path type without validation. In a large application where many handlers share the `Env`, create them from a factory instead of writing the `Env` each time. See [`factory.defineHandler()`](/docs/helpers/factory#factory-definehandler). +Inline handlers can use `defineHandler()` too, to return a plain value, to validate the request, or to put middleware next to the handler. The next sections show these. + ## Validate in `defineHandler()` Put the validation in `defineHandler()` instead of a validator middleware. Pass a [Standard Schema](https://standardschema.dev/), such as a Zod schema, for each target of the request. The validated values come as the second argument with the types, and the request is rejected with `400 Bad Request` before the handler runs. From 7a60d7e902a71f0d80da0a5233d8508b1c077429 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:34:09 +0900 Subject: [PATCH 17/29] docs: compare the same handler in the first section --- docs/guides/best-practices.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index 5661e558..cef40329 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -17,39 +17,38 @@ const bookPermalink = (c: Context) => { app.get('/books/:id', bookPermalink) ``` -Define it with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. The types stay, and the returned value is converted to a Response, so you can return a plain object. +Define it with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. The path is not known there either, so validate `param` and the parameter is typed. The returned value is converted to a Response, so you can return a plain object. ```ts import { defineHandler } from 'hono/factory' +import * as z from 'zod' // 😃 -const listBooks = defineHandler(async () => await db.books.list()) +const bookPermalink = defineHandler({ + param: z.object({ id: z.string() }), +})(async (c, { param }) => { + return await db.books.find(param.id) // `param.id` is `string` +}) -app.get('/books', listBooks) +app.get('/books/:id', bookPermalink) ``` -Pass the `Env` as a type argument when the handler needs it. +With nothing to validate, `defineHandler()` alone is enough. ```ts // 😃 -const me = defineHandler((c) => c.get('user')) +const listBooks = defineHandler(async () => await db.books.list()) -app.get('/me', me) +app.get('/books', listBooks) ``` -The path is not known there, so validate `param` to type the parameter. +Pass the `Env` as a type argument when the handler needs it. ```ts -import * as z from 'zod' - // 😃 -const bookPermalink = defineHandler({ - param: z.object({ id: z.string() }), -})(async (c, { param }) => { - return await db.books.find(param.id) // `param.id` is `string` -}) +const me = defineHandler((c) => c.get('user')) -app.get('/books/:id', bookPermalink) +app.get('/me', me) ``` Pass the path as the second type argument, `defineHandler`, only when you need the path type without validation. In a large application where many handlers share the `Env`, create them from a factory instead of writing the `Env` each time. See [`factory.defineHandler()`](/docs/helpers/factory#factory-definehandler). From 487b0a200e37b4adaeca08570fa4520d2f9eb49d Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:36:12 +0900 Subject: [PATCH 18/29] docs: validate the request value, so use defineHandler() --- docs/guides/best-practices.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index cef40329..6f4d1c9c 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -5,7 +5,7 @@ However, there are best practices that are better to follow. ## Define handlers with `defineHandler()` -A handler written inline is typed by `app.get()`. Once you move it to a variable, a plain function loses the types. The `Env` is unknown, and the path parameter cannot be inferred. +A handler written inline is typed by `app.get()`. Once you move it to a variable, the common way is to type the argument as `Context`. That loses the types: the `Env` is unknown, and the path parameter cannot be inferred. ```ts // 🙁 @@ -17,7 +17,7 @@ const bookPermalink = (c: Context) => { app.get('/books/:id', bookPermalink) ``` -Define it with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. The path is not known there either, so validate `param` and the parameter is typed. The returned value is converted to a Response, so you can return a plain object. +A value from the request should be validated anyway. Define the handler with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory` and validate `param` there. The parameter is checked at runtime and typed in the handler. The returned value is converted to a Response, so you can return a plain object. ```ts import { defineHandler } from 'hono/factory' From 14b60ad39cb7190f6d10cc7f01a2705814d2b449 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:37:58 +0900 Subject: [PATCH 19/29] docs: mention createFactory() for a shared Env --- docs/guides/best-practices.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index 6f4d1c9c..94758bc2 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -51,7 +51,18 @@ const me = defineHandler((c) => c.get('user')) app.get('/me', me) ``` -Pass the path as the second type argument, `defineHandler`, only when you need the path type without validation. In a large application where many handlers share the `Env`, create them from a factory instead of writing the `Env` each time. See [`factory.defineHandler()`](/docs/helpers/factory#factory-definehandler). +In a large application where many handlers share the `Env`, create them from a factory and write the `Env` once. See [`factory.defineHandler()`](/docs/helpers/factory#factory-definehandler). + +```ts +import { createFactory } from 'hono/factory' + +const factory = createFactory() + +// 😃 +const me = factory.defineHandler((c) => c.get('user')) +``` + +Pass the path as the second type argument, `defineHandler`, only when you need the path type without validation. Inline handlers can use `defineHandler()` too, to return a plain value, to validate the request, or to put middleware next to the handler. The next sections show these. From 156cec1a3ae83134070ac6ea4708629b093e206c Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:41:15 +0900 Subject: [PATCH 20/29] docs: write the Validate and middleware examples inline --- docs/guides/best-practices.md | 49 +++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index 94758bc2..dcb232ca 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -74,15 +74,16 @@ Put the validation in `defineHandler()` instead of a validator middleware. Pass import * as z from 'zod' // 😃 -const createBook = defineHandler({ - json: z.object({ title: z.string(), author: z.string() }), -})(async (c, { json }) => { - const book = await db.books.create(json) // `{ title: string; author: string }` - c.status(201) - return book -}) - -app.post('/books', createBook) +app.post( + '/books', + defineHandler({ + json: z.object({ title: z.string(), author: z.string() }), + })(async (c, { json }) => { + const book = await db.books.create(json) // `{ title: string; author: string }` + c.status(201) + return book + }) +) ``` Add `response` when the shape of the response matters, for example when it is a public API. The returned value is validated, and extra fields are stripped by the schema. @@ -91,12 +92,15 @@ Add `response` when the shape of the response matters, for example when it is a const BookSchema = z.object({ id: z.string(), title: z.string() }) // 😃 -const bookPermalink = defineHandler({ - param: z.object({ id: z.string() }), - response: BookSchema, -})(async (c, { param }) => { - return await db.books.find(param.id) // Only `id` and `title` are sent -}) +app.get( + '/books/:id', + defineHandler({ + param: z.object({ id: z.string() }), + response: BookSchema, + })(async (c, { param }) => { + return await db.books.find(param.id) // Only `id` and `title` are sent + }) +) ``` ## Put middleware next to the handler @@ -114,13 +118,14 @@ const auth = defineMiddleware<{ Variables: { user: User } }>( ) // 😃 -const createBook = defineHandler({ - json: z.object({ title: z.string() }), -})(auth, async (c, { json }) => { - return await db.books.create({ ...json, owner: c.get('user').id }) -}) - -app.post('/books', createBook) +app.post( + '/books', + defineHandler({ + json: z.object({ title: z.string() }), + })(auth, async (c, { json }) => { + return await db.books.create({ ...json, owner: c.get('user').id }) + }) +) ``` ## Building a larger application From 0fc9a2b7abf9bbe3758d3361ea914b4649091152 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:45:29 +0900 Subject: [PATCH 21/29] docs: start Best Practices with defineHandler(), without the Controller framing --- docs/guides/best-practices.md | 90 ++++++++--------------------------- 1 file changed, 19 insertions(+), 71 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index dcb232ca..b9096e9e 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -3,77 +3,14 @@ Hono is very flexible. You can write your app as you like. However, there are best practices that are better to follow. -## Define handlers with `defineHandler()` +## Use `defineHandler()` -A handler written inline is typed by `app.get()`. Once you move it to a variable, the common way is to type the argument as `Context`. That loses the types: the `Env` is unknown, and the path parameter cannot be inferred. - -```ts -// 🙁 -const bookPermalink = (c: Context) => { - const id = c.req.param('id') // Can't infer the path param - return c.json(`get ${id}`) -} - -app.get('/books/:id', bookPermalink) -``` - -A value from the request should be validated anyway. Define the handler with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory` and validate `param` there. The parameter is checked at runtime and typed in the handler. The returned value is converted to a Response, so you can return a plain object. +Write handlers with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. A value from the request should be validated, so validate it there: pass a [Standard Schema](https://standardschema.dev/), such as a Zod schema, for each target of the request, like `param`, `query`, or `json`. The validated values come as the second argument with the types, and the request is rejected with `400 Bad Request` before the handler runs. The returned value is converted to a Response, so you can return a plain object. ```ts import { defineHandler } from 'hono/factory' import * as z from 'zod' -// 😃 -const bookPermalink = defineHandler({ - param: z.object({ id: z.string() }), -})(async (c, { param }) => { - return await db.books.find(param.id) // `param.id` is `string` -}) - -app.get('/books/:id', bookPermalink) -``` - -With nothing to validate, `defineHandler()` alone is enough. - -```ts -// 😃 -const listBooks = defineHandler(async () => await db.books.list()) - -app.get('/books', listBooks) -``` - -Pass the `Env` as a type argument when the handler needs it. - -```ts -// 😃 -const me = defineHandler((c) => c.get('user')) - -app.get('/me', me) -``` - -In a large application where many handlers share the `Env`, create them from a factory and write the `Env` once. See [`factory.defineHandler()`](/docs/helpers/factory#factory-definehandler). - -```ts -import { createFactory } from 'hono/factory' - -const factory = createFactory() - -// 😃 -const me = factory.defineHandler((c) => c.get('user')) -``` - -Pass the path as the second type argument, `defineHandler`, only when you need the path type without validation. - -Inline handlers can use `defineHandler()` too, to return a plain value, to validate the request, or to put middleware next to the handler. The next sections show these. - -## Validate in `defineHandler()` - -Put the validation in `defineHandler()` instead of a validator middleware. Pass a [Standard Schema](https://standardschema.dev/), such as a Zod schema, for each target of the request. The validated values come as the second argument with the types, and the request is rejected with `400 Bad Request` before the handler runs. - -```ts -import * as z from 'zod' - -// 😃 app.post( '/books', defineHandler({ @@ -86,12 +23,20 @@ app.post( ) ``` +With nothing to validate, `defineHandler()` alone is enough. + +```ts +app.get( + '/books', + defineHandler(async () => await db.books.list()) +) +``` + Add `response` when the shape of the response matters, for example when it is a public API. The returned value is validated, and extra fields are stripped by the schema. ```ts const BookSchema = z.object({ id: z.string(), title: z.string() }) -// 😃 app.get( '/books/:id', defineHandler({ @@ -117,7 +62,6 @@ const auth = defineMiddleware<{ Variables: { user: User } }>( } ) -// 😃 app.post( '/books', defineHandler({ @@ -213,19 +157,23 @@ app.route('/books', books) export default app ``` -When the handlers grow, move them out of the route file. A handler defined with `defineHandler()` keeps its types in any file. +When the handlers grow, move them out of the route file. A handler defined with `defineHandler()` keeps its types in any file, so do not type the argument as `Context`. Outside `app.get()`, the handler does not know the `Env` of the app. Pass it as a type argument, `defineHandler()`, or create the handlers from a factory to write the `Env` once. ```ts // books/handlers.ts -export const listBooks = defineHandler( +import { createFactory } from 'hono/factory' + +const factory = createFactory() + +export const listBooks = factory.defineHandler( async () => await db.books.list() ) -export const createBook = defineHandler({ +export const createBook = factory.defineHandler({ json: z.object({ title: z.string() }), })(async (c, { json }) => { c.status(201) - return await db.books.create(json) + return await db.books.create({ ...json, owner: c.get('user').id }) }) ``` From d36308d2c9308cf48e13e84279f7132a02421d7d Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:59:14 +0900 Subject: [PATCH 22/29] docs: mention Valibot --- docs/guides/best-practices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index b9096e9e..240a03f5 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -5,7 +5,7 @@ However, there are best practices that are better to follow. ## Use `defineHandler()` -Write handlers with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. A value from the request should be validated, so validate it there: pass a [Standard Schema](https://standardschema.dev/), such as a Zod schema, for each target of the request, like `param`, `query`, or `json`. The validated values come as the second argument with the types, and the request is rejected with `400 Bad Request` before the handler runs. The returned value is converted to a Response, so you can return a plain object. +Write handlers with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. A value from the request should be validated, so validate it there: pass a [Standard Schema](https://standardschema.dev/), such as a Zod or Valibot schema, for each target of the request, like `param`, `query`, or `json`. The validated values come as the second argument with the types, and the request is rejected with `400 Bad Request` before the handler runs. The returned value is converted to a Response, so you can return a plain object. ```ts import { defineHandler } from 'hono/factory' From 184555c0fdd66c401455b9c95466f417d7e92873 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 19:59:48 +0900 Subject: [PATCH 23/29] docs: keep the example comment inside the code block width --- docs/guides/best-practices.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index 240a03f5..6fdabfc2 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -16,7 +16,8 @@ app.post( defineHandler({ json: z.object({ title: z.string(), author: z.string() }), })(async (c, { json }) => { - const book = await db.books.create(json) // `{ title: string; author: string }` + // `json` is `{ title: string; author: string }` + const book = await db.books.create(json) c.status(201) return book }) @@ -43,7 +44,8 @@ app.get( param: z.object({ id: z.string() }), response: BookSchema, })(async (c, { param }) => { - return await db.books.find(param.id) // Only `id` and `title` are sent + // Only `id` and `title` are sent + return await db.books.find(param.id) }) ) ``` From cf94ddbc29d10bf99fc3ab8dd34f3b737980f75b Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 20:05:06 +0900 Subject: [PATCH 24/29] docs: drop the deno.land/x note from the v5 migration guide --- docs/guides/migrating-to-v5.md | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/docs/guides/migrating-to-v5.md b/docs/guides/migrating-to-v5.md index 782ae544..6783f52b 100644 --- a/docs/guides/migrating-to-v5.md +++ b/docs/guides/migrating-to-v5.md @@ -50,10 +50,10 @@ A non-Error value thrown from a handler or middleware, such as a string or a pla `getColorEnabledAsync()` from `hono/utils/color` is removed. Use `getColorEnabled()` and pass the bindings on Cloudflare Workers. The `logger()` middleware does this by itself, so it needs no change. ```ts -// From +// Before const enabled = await getColorEnabledAsync() -// To +// After const enabled = getColorEnabled(c.env) ``` @@ -151,15 +151,3 @@ toSSG(app, fs, { plugins: [{ beforeRequestHook }, defaultPlugin()] }) - `timingSafeEqual()` in `hono/utils/buffer` only accepts strings. The `hashFunction` option of the Basic Auth and Bearer Auth middleware is typed as `(input: string) => string | null | Promise` accordingly. - `getQueryStrings()` in `hono/utils/url` is removed. Use the `URL` API instead. - `UnOfficalStatusCode` in `hono/utils/http-status` is removed. Use `UnofficialStatusCode` instead. - -## `deno.land/x` is no longer published - -This is not a breaking change, but Hono is no longer published to `deno.land/x`. On Deno, use [JSR](/docs/getting-started/deno) instead. - -```ts -// From -import { Hono } from 'https://deno.land/x/hono/mod.ts' - -// To -import { Hono } from 'jsr:@hono/hono' -``` From 62287824643040ee8a1d931d2b2118cb7169757a Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Thu, 8 Oct 2026 20:14:34 +0900 Subject: [PATCH 25/29] docs(agents): follow @hono/cli rc.1, without optimize --- docs/agents/cli.md | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/docs/agents/cli.md b/docs/agents/cli.md index 57303d75..833b334c 100644 --- a/docs/agents/cli.md +++ b/docs/agents/cli.md @@ -1,6 +1,6 @@ # Hono CLI -Hono CLI (`hono`) is a command-line tool for Hono, made for coding agents. It loads your Hono app directly, so an agent can inspect, test, and build the app without starting a server. Commands print JSON by default. `routes`, `request`, `benchmark`, `optimize`, and `ssg` take `--plain` when a human reads the output. +Hono CLI (`hono`) is a command-line tool for Hono, made for coding agents. It loads your Hono app directly, so an agent can inspect and test the app without starting a server. Commands print JSON by default. `routes`, `request`, `benchmark`, and `ssg` take `--plain` when a human reads the output. ## Installation @@ -36,7 +36,6 @@ On failure, `error.suggestions` says what to try next, and `error.docs` points t | `hono batch [file]` | Run multiple requests from JSONL using `app.request()` | | `hono snapshot [file]` | Print the current behavior as batch JSONL lines | | `hono benchmark [file]` | Measure the performance of your Hono app | -| `hono optimize [entry]` | Build an optimized Hono app | | `hono ssg [file]` | Generate static files from your Hono app | `file` is the path to your app file. When omitted, the app is found in `src/index.ts`, `src/index.tsx`, `src/index.js`, or `src/index.jsx`. TypeScript and JSX are supported. @@ -125,7 +124,7 @@ echo 'app.get("/hello", (c) => c.json({ ok: true }))' | hono request /hello - #### Cloudflare bindings -In a project with a wrangler config, `c.env` carries the real local bindings (KV, D1, R2, vars) automatically, while the app runs on Node.js. This works in `request`, `batch`, and `snapshot`. Skip it with `--no-bindings`. It needs [wrangler](https://developers.cloudflare.com/workers/wrangler/) installed in the project. +In a project with a wrangler config, `c.env` carries the real local bindings (KV, D1, R2, vars) automatically, while the app runs on Node.js. This works in `request`, `batch`, `snapshot`, and `ssg`. Skip it with `--no-bindings`. It needs [wrangler](https://developers.cloudflare.com/workers/wrangler/) installed in the project. `--runtime workerd` runs the whole app inside workerd instead, with the wrangler config. It is heavier, but it is the full runtime. The entry is `main` in the wrangler config, so pass no file argument. @@ -193,22 +192,6 @@ hono benchmark --hono ../hono A few percent of difference is noise. To compare, run it more than once and check that the difference repeats. -### optimize - -Build an optimized Hono app. The router is replaced with a prepared one, and unused Hono APIs are removed. For a plain bundle, use your normal build tool. This command exists for the Hono-specific optimizations. - -```sh -hono optimize -hono optimize -m -o dist/app.js -``` - -- `-o, --outfile ` - output file (default: `dist/index.js`) -- `-m, --minify` - minify the output -- `-t, --target ` - environment target, such as `node24`, `deno2`, or `es2024` (default: `node20`) -- `--request-body-api-removal ` - `auto` (default), `force`, or `disable`. Request body APIs are removed only when every route method is strictly GET, HEAD, or OPTIONS. -- `--no-hono-api-removal` - keep unused Hono APIs -- `--no-context-response-api-removal` - keep unused response methods of `Context` - ### ssg Generate static files from your Hono app, like the [SSG helper](/docs/helpers/ssg). @@ -221,3 +204,17 @@ hono ssg --exclude '/api/*' - `-o, --outdir ` - output directory (default: `static`) - `--include ` / `--exclude ` - select routes by path. `*` matches anything +- `--no-bindings` - skip loading the local Cloudflare bindings + +A page that does not answer 200 is not written. It is listed in `skipped` with its status, so check it with `hono request `. + +```json +{ + "ok": true, + "data": { + "output": "static", + "files": ["static/index.html", "static/about.html"], + "skipped": [{ "path": "/counter", "status": 500 }] + } +} +``` From 09d0d5b6ccb1d4ff5ece2840f37f974cdf79da1b Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Sat, 10 Oct 2026 21:14:00 +0900 Subject: [PATCH 26/29] docs(agents): add --runtime vite from @hono/cli rc.1 --- docs/agents/cli.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/agents/cli.md b/docs/agents/cli.md index 833b334c..2c359003 100644 --- a/docs/agents/cli.md +++ b/docs/agents/cli.md @@ -94,7 +94,7 @@ hono request / --runtime bun - `-d, --data ` - request body (`@file` reads a file, `@-` reads stdin) - `-H, --header
` - custom header (can be used multiple times) - `--trace` - include the matched routes in the output -- `--runtime ` - run the app on `node` (default), `bun`, `deno`, or `workerd` +- `--runtime ` - run the app on `node` (default), `bun`, `deno`, `workerd`, or `vite` - `--compact` - one-line JSON without the headers - `--no-bindings` - skip loading the local Cloudflare bindings - `-w, --watch` - watch for changes and resend the request @@ -128,6 +128,10 @@ In a project with a wrangler config, `c.env` carries the real local bindings (KV `--runtime workerd` runs the whole app inside workerd instead, with the wrangler config. It is heavier, but it is the full runtime. The entry is `main` in the wrangler config, so pass no file argument. +#### Vite + +`--runtime vite` sends the requests through the Vite dev server of the project, for an app that a Vite plugin builds. The app comes from the Vite config, so pass no file argument. In a project with `cloudflare.config.ts` and a Vite config, as `cf init` makes, it is the default, and `c.env` has the bindings. This works in `request`, `batch`, and `snapshot`. A file argument, `--no-bindings`, `--trace`, or `--watch` runs the app on Node.js instead. + ### batch Run multiple requests from JSONL in one call, in order, against one app instance. In-memory state carries between steps. @@ -152,7 +156,7 @@ The output has the actual `status` and `body` of each step, `pass` per step, and - `-H, --header
` - a shared header for every step - `--compact` - print only the failed steps and the summary -- `--runtime ` - `node` (default) or `workerd` +- `--runtime ` - `node` (default), `workerd`, or `vite` - `--no-bindings` - skip loading the local Cloudflare bindings ### snapshot @@ -169,7 +173,7 @@ Paramless GET routes are executed, and their actual status and body become the ` Capture before a refactor, then rerun the lines with `hono batch` until `failed` is 0. - `--status-only` - capture only the status codes, not the bodies -- `--runtime ` - `node` (default) or `workerd` +- `--runtime ` - `node` (default), `workerd`, or `vite` - `--no-bindings` - skip loading the local Cloudflare bindings ### benchmark From 7e745515c54af286f921d6402eec869579a0e907 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Sat, 10 Oct 2026 21:18:34 +0900 Subject: [PATCH 27/29] docs(agents): do not point readers to cf init --- docs/agents/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/agents/cli.md b/docs/agents/cli.md index 2c359003..3b75efe2 100644 --- a/docs/agents/cli.md +++ b/docs/agents/cli.md @@ -130,7 +130,7 @@ In a project with a wrangler config, `c.env` carries the real local bindings (KV #### Vite -`--runtime vite` sends the requests through the Vite dev server of the project, for an app that a Vite plugin builds. The app comes from the Vite config, so pass no file argument. In a project with `cloudflare.config.ts` and a Vite config, as `cf init` makes, it is the default, and `c.env` has the bindings. This works in `request`, `batch`, and `snapshot`. A file argument, `--no-bindings`, `--trace`, or `--watch` runs the app on Node.js instead. +`--runtime vite` sends the requests through the Vite dev server of the project, for an app that a Vite plugin builds. The app comes from the Vite config, so pass no file argument. In a project with `cloudflare.config.ts` and a Vite config, it is the default, and `c.env` has the bindings. This works in `request`, `batch`, and `snapshot`. A file argument, `--no-bindings`, `--trace`, or `--watch` runs the app on Node.js instead. ### batch From 134bd9688beed917d4b8e8f90f930fcaa7f58c80 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Sat, 10 Oct 2026 21:36:18 +0900 Subject: [PATCH 28/29] docs: follow hono v5 rc.1 in the migration guide and the factory helper --- docs/guides/migrating-to-v5.md | 56 +++++++++++++++++++++++++++++++++- docs/helpers/factory.md | 5 +-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/docs/guides/migrating-to-v5.md b/docs/guides/migrating-to-v5.md index 6783f52b..f2603589 100644 --- a/docs/guides/migrating-to-v5.md +++ b/docs/guides/migrating-to-v5.md @@ -41,9 +41,49 @@ import { serveStatic } from '@hono/bun' `hono/adapter` (`env()` and `getRuntimeKey()`) stays in `hono`. +## `onError()` and `notFound()` register routable middleware + +`onError()` now accepts middleware with the `(c, next)` signature instead of `(err, c)`. Read the error from `c.error`: + +```ts +// From +app.onError((err, c) => { + console.error(err) + return c.text('Custom Error', 500) +}) + +// To +app.onError((c) => { + console.error(c.error) + return c.text('Custom Error', 500) +}) +``` + +`notFound((c) => c.text('Not Found', 404))` keeps the same single-handler syntax. Both APIs also accept an optional path and multiple middleware: + +```ts +app.notFound( + '/api/*', + async (c, next) => { + c.header('x-not-found', 'api') + await next() + }, + (c) => c.text('API resource not found', 404) +) +``` + +The following routing rules apply to both APIs: + +- Registrations compose rather than replacing the previous handler. Return a response to stop the chain, or call `next()` to delegate. +- Paths are independent of the request's HTTP method and relative to `basePath()`. Omitting the path registers `*` within the current base path. +- `route()` imports sub-app handlers, including `notFound()` handlers. More deeply nested sub-apps take priority; at the same depth, handlers run in registration order, not path specificity order. +- Scopes match the request path, regardless of which app registered the original route. Request parameters still come from the original route, not the fallback scope. + +`notFound()` middleware runs for both unmatched requests and explicit `c.notFound()` calls. If the chain reaches its end without a finalized response, the built-in error or not-found handler is used. Errors thrown by `onError()` middleware are handled by the built-in error handler rather than escaping from `app.fetch()`. + ## Non-Error throws go to `onError` -A non-Error value thrown from a handler or middleware, such as a string or a plain object, now goes to `onError` wrapped in an `Error`. The original value is available as `err.cause`, and a thrown string is also used as `err.message`. It no longer propagates out of `app.fetch()`. +A non-Error value thrown from a handler or middleware, such as a string or a plain object, now goes to `onError` wrapped in an `Error`. The original value is available as `c.error.cause`, and a thrown string is also used as `c.error.message`. It no longer propagates out of `app.fetch()`. ## `getColorEnabledAsync()` is removed @@ -85,6 +125,20 @@ const body = await c.req.json<{ name: string }>() `c.json(undefined)` used to return an empty body. It now throws a `TypeError`, like `Response.json()`. The same applies to a function or a symbol. +## Wildcard matching is the same in every router + +A `*` at the end of a segment is a wildcard in every router, and it may match nothing. A `*` in the middle of a segment, such as `/x*y`, is not a wildcard. A segment that is only `*` matches one non-empty segment. + +| Route | Path | v5 | +| -------------- | ------------- | -------- | +| `/x*/y` | `/x/y` | Match | +| `/x*/y` | `/xz/y` | Match | +| `/x*/y` | `/x/z/y` | No match | +| `/x*y` | `/xay` | No match | +| `/wild/*/card` | `/wild//card` | No match | + +TrieRouter and PatternRouter did not match `/x*/y` to `/xz/y` before. In RegExpRouter, `/x*/y` and `/x/*` can not be registered together and throw `UnsupportedPathError`, so SmartRouter falls back to TrieRouter. + ## Removed deprecated features ### `app.mount()` diff --git a/docs/helpers/factory.md b/docs/helpers/factory.md index f840809d..00aa15f9 100644 --- a/docs/helpers/factory.md +++ b/docs/helpers/factory.md @@ -119,10 +119,11 @@ When the request fails validation, the handler does not run and the response is When the returned value fails validation, the response is `500 Internal Server Error` with `{ "error": "Response validation failed" }` and without the issues. -Both are thrown as an [`HTTPException`](/docs/api/exception), so you can customize the response in `app.onError()`. The raw issues are in `err.cause.issues`. +Both are thrown as an [`HTTPException`](/docs/api/exception), so you can customize the response in `app.onError()`. The raw issues are in `c.error.cause.issues`. ```ts -app.onError((err, c) => { +app.onError((c) => { + const err = c.error if (err instanceof HTTPException && err.status === 400) { console.log(err.cause) // { issues: [{ slot: 'json', issues: [...] }] } return c.json({ message: 'Bad Request' }, 400) From 63383a9790630a098d2d1af88d46a53175424609 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Sat, 10 Oct 2026 21:45:49 +0900 Subject: [PATCH 29/29] docs: hold the v5-only pages for GA Best Practices, the Factory Helper defineHandler() sections, and the Getting Started note about AGENTS.md describe hono@next and create-hono@next. They come back when v5 is latest. --- docs/guides/best-practices.md | 217 +++++++----------------- docs/helpers/factory.md | 300 +++++----------------------------- docs/index.md | 4 +- index.md | 2 +- 4 files changed, 97 insertions(+), 426 deletions(-) diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md index 6fdabfc2..cccd045a 100644 --- a/docs/guides/best-practices.md +++ b/docs/guides/best-practices.md @@ -3,110 +3,81 @@ Hono is very flexible. You can write your app as you like. However, there are best practices that are better to follow. -## Use `defineHandler()` +## Don't make "Controllers" when possible -Write handlers with [`defineHandler()`](/docs/helpers/factory#definehandler) from `hono/factory`. A value from the request should be validated, so validate it there: pass a [Standard Schema](https://standardschema.dev/), such as a Zod or Valibot schema, for each target of the request, like `param`, `query`, or `json`. The validated values come as the second argument with the types, and the request is rejected with `400 Bad Request` before the handler runs. The returned value is converted to a Response, so you can return a plain object. +When possible, you should not create "Ruby on Rails-like Controllers". ```ts -import { defineHandler } from 'hono/factory' -import * as z from 'zod' - -app.post( - '/books', - defineHandler({ - json: z.object({ title: z.string(), author: z.string() }), - })(async (c, { json }) => { - // `json` is `{ title: string; author: string }` - const book = await db.books.create(json) - c.status(201) - return book - }) -) +// 🙁 +// A RoR-like Controller +const booksList = (c: Context) => { + return c.json('list books') +} + +app.get('/books', booksList) ``` -With nothing to validate, `defineHandler()` alone is enough. +The issue is related to types. For example, the path parameter cannot be inferred in the Controller without writing complex generics. ```ts -app.get( - '/books', - defineHandler(async () => await db.books.list()) -) +// 🙁 +// A RoR-like Controller +const bookPermalink = (c: Context) => { + const id = c.req.param('id') // Can't infer the path param + return c.json(`get ${id}`) +} ``` -Add `response` when the shape of the response matters, for example when it is a public API. The returned value is validated, and extra fields are stripped by the schema. +Therefore, you don't need to create RoR-like controllers and should write handlers directly after path definitions. ```ts -const BookSchema = z.object({ id: z.string(), title: z.string() }) - -app.get( - '/books/:id', - defineHandler({ - param: z.object({ id: z.string() }), - response: BookSchema, - })(async (c, { param }) => { - // Only `id` and `title` are sent - return await db.books.find(param.id) - }) -) +// 😃 +app.get('/books/:id', (c) => { + const id = c.req.param('id') // Can infer the path param + return c.json(`get ${id}`) +}) ``` -## Put middleware next to the handler +## `factory.createHandlers()` in `hono/factory` -Middleware that belongs to one handler, such as authentication, goes before the handler in `defineHandler()`. The `Env` of the middleware flows into the handler, so `c.get()` is typed. Middleware for many routes still goes in `app.use()`. +If you still want to create a RoR-like Controller, use `factory.createHandlers()` in [`hono/factory`](/docs/helpers/factory). If you use this, type inference will work correctly. ```ts -import { defineHandler, defineMiddleware } from 'hono/factory' +import { createFactory } from 'hono/factory' +import { logger } from 'hono/logger' -const auth = defineMiddleware<{ Variables: { user: User } }>( - async (c, next) => { - c.set('user', await getUser(c)) - await next() - } -) - -app.post( - '/books', - defineHandler({ - json: z.object({ title: z.string() }), - })(auth, async (c, { json }) => { - return await db.books.create({ ...json, owner: c.get('user').id }) - }) -) +// ... + +// 😃 +const factory = createFactory() + +const middleware = factory.createMiddleware(async (c, next) => { + c.set('foo', 'bar') + await next() +}) + +const handlers = factory.createHandlers(logger(), middleware, (c) => { + return c.json(c.var.foo) +}) + +app.get('/api', ...handlers) ``` ## Building a larger application -Use `app.route()` to build a larger application. Each resource gets its own file with a Hono instance and its handlers. +Use `app.route()` to build a larger application without creating "Ruby on Rails-like Controllers". If your application has `/authors` and `/books` endpoints and you wish to separate files from `index.ts`, create `authors.ts` and `books.ts`. ```ts // authors.ts import { Hono } from 'hono' -import { defineHandler } from 'hono/factory' -import * as z from 'zod' const app = new Hono() -app.get( - '/', - defineHandler(async () => await db.authors.list()) -) -app.post( - '/', - defineHandler({ json: z.object({ name: z.string() }) })( - async (c, { json }) => { - c.status(201) - return await db.authors.create(json) - } - ) -) -app.get( - '/:id', - defineHandler({ param: z.object({ id: z.string() }) })( - async (c, { param }) => await db.authors.find(param.id) - ) -) +app.get('/', (c) => c.json('list authors')) +app.post('/', (c) => c.json('create an author', 201)) +app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`)) export default app ``` @@ -114,30 +85,12 @@ export default app ```ts // books.ts import { Hono } from 'hono' -import { defineHandler } from 'hono/factory' -import * as z from 'zod' const app = new Hono() -app.get( - '/', - defineHandler(async () => await db.books.list()) -) -app.post( - '/', - defineHandler({ json: z.object({ title: z.string() }) })( - async (c, { json }) => { - c.status(201) - return await db.books.create(json) - } - ) -) -app.get( - '/:id', - defineHandler({ param: z.object({ id: z.string() }) })( - async (c, { param }) => await db.books.find(param.id) - ) -) +app.get('/', (c) => c.json('list books')) +app.post('/', (c) => c.json('create a book', 201)) +app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`)) export default app ``` @@ -159,69 +112,19 @@ app.route('/books', books) export default app ``` -When the handlers grow, move them out of the route file. A handler defined with `defineHandler()` keeps its types in any file, so do not type the argument as `Context`. Outside `app.get()`, the handler does not know the `Env` of the app. Pass it as a type argument, `defineHandler()`, or create the handlers from a factory to write the `Env` once. - -```ts -// books/handlers.ts -import { createFactory } from 'hono/factory' - -const factory = createFactory() - -export const listBooks = factory.defineHandler( - async () => await db.books.list() -) - -export const createBook = factory.defineHandler({ - json: z.object({ title: z.string() }), -})(async (c, { json }) => { - c.status(201) - return await db.books.create({ ...json, owner: c.get('user').id }) -}) -``` - -```ts -// books/index.ts -import { createBook, listBooks } from './handlers' - -const app = new Hono() - -app.get('/', listBooks) -app.post('/', createBook) - -export default app -``` - ### If you want to use RPC features The code above works well for normal use cases. -However, if you want to use the `RPC` feature, you can get the correct type by chaining as follows. `defineHandler()` gives the client the validated input and the returned value. +However, if you want to use the `RPC` feature, you can get the correct type by chaining as follows. ```ts // authors.ts import { Hono } from 'hono' -import { defineHandler } from 'hono/factory' -import * as z from 'zod' const app = new Hono() - .get( - '/', - defineHandler(async () => await db.authors.list()) - ) - .post( - '/', - defineHandler({ json: z.object({ name: z.string() }) })( - async (c, { json }) => { - c.status(201) - return await db.authors.create(json) - } - ) - ) - .get( - '/:id', - defineHandler({ param: z.object({ id: z.string() }) })( - async (c, { param }) => await db.authors.find(param.id) - ) - ) + .get('/', (c) => c.json('list authors')) + .post('/', (c) => c.json('create an author', 201)) + .get('/:id', (c) => c.json(`get ${c.req.param('id')}`)) export default app export type AppType = typeof app @@ -235,9 +138,6 @@ import { hc } from 'hono/client' // 😃 const client = hc('http://localhost') // Typed correctly - -const res = await client.index.$post({ json: { name: 'Yusuke' } }) -const author = await res.json() // The type of `db.authors.create()` ``` For more detailed information, please see [the RPC page](/docs/guides/rpc#using-rpc-with-larger-applications). @@ -252,14 +152,11 @@ Hono automatically handles HEAD requests by converting them to GET requests and ```typescript // GOOD: This GET route automatically handles HEAD requests -app.get( - '/api/users', - defineHandler(async (c) => { - const users = await getUsers() - c.header('X-Total-Count', users.length.toString()) - return users - }) -) +app.get('/api/users', async (c) => { + const users = await getUsers() + c.header('X-Total-Count', users.length.toString()) + return c.json(users) +}) // HEAD /api/users will return: // - Same headers as GET (including X-Total-Count) diff --git a/docs/helpers/factory.md b/docs/helpers/factory.md index 00aa15f9..a19f2329 100644 --- a/docs/helpers/factory.md +++ b/docs/helpers/factory.md @@ -1,208 +1,57 @@ # Factory Helper -The Factory Helper provides functions for defining Hono's components, such as handlers and middleware, with the proper TypeScript types. +The Factory Helper provides useful functions for creating Hono's components such as Middleware. Sometimes it's difficult to set the proper TypeScript types, but this helper facilitates that. ## Import ```ts import { Hono } from 'hono' -import { - createFactory, - defineHandler, - defineMiddleware, -} from 'hono/factory' +import { createFactory, createMiddleware } from 'hono/factory' ``` -## `defineHandler()` +## `createFactory()` -`defineHandler()` defines a handler. The value returned from the handler is converted to a Response, so you can return a plain object or a string. +`createFactory()` will create an instance of the Factory class. ```ts -import { defineHandler } from 'hono/factory' - -app.get( - '/ping', - defineHandler(() => ({ pong: true })) -) -``` - -The returned value is converted as follows: - -| Returned value | Response | -| --------------------- | --------------------- | -| An object | JSON, like `c.json()` | -| A string or JSX | HTML, like `c.html()` | -| `null` or `undefined` | `204 No Content` | -| A `Response` | Returned as is | - -To set the status, call `c.status()` before returning. The status is not included in the types, so return `c.json()` when you want the status in the types for the [RPC](/docs/guides/rpc) client. +import { createFactory } from 'hono/factory' -```ts -app.post( - '/users', - defineHandler((c) => { - c.status(201) - return { id: '1' } - }) -) +const factory = createFactory() ``` -### Middleware - -Pass middleware before the handler. The middleware runs first, and its `Env` flows into the handler, so `c.get()` is typed. +You can pass your Env types as Generics: ```ts -const auth = defineMiddleware<{ Variables: { user: User } }>( - async (c, next) => { - c.set('user', await getUser(c)) - await next() +type Env = { + Variables: { + foo: string } -) - -app.get( - '/me', - defineHandler(auth, (c) => c.get('user')) // `User` -) -``` - -### Validation - -Pass an options object to validate the request before the handler runs. Each option is a [Standard Schema](https://standardschema.dev/), such as a Zod or Valibot schema, for one target of the request: `param`, `query`, `header`, `cookie`, `form`, or `json`. The validated values are passed to the handler as the second argument, and are also available through `c.req.valid()`. - -```ts -import * as z from 'zod' - -app.post( - '/users/:id', - defineHandler({ - param: z.object({ id: z.string() }), - query: z.object({ page: z.coerce.number() }), - json: z.object({ name: z.string() }), - })((c, { param, query, json }) => { - return { id: param.id, page: query.page, name: json.name } - }) -) -``` - -Add `response` to validate the returned value. The value is validated, transformed by the schema, and returned as JSON. With `response`, the handler can return only a value of the schema's input type or a non-JSON `Response` like `c.redirect()`. - -```ts -app.get( - '/users/:id', - defineHandler({ - param: z.object({ id: z.string() }), - response: z.object({ id: z.string(), name: z.string() }), - })(async (c, { param }) => { - return await findUser(param.id) - }) -) -``` - -When the request fails validation, the handler does not run and the response is `400 Bad Request` with the issues of every failed target. The input values are not included. - -```json -{ - "error": "Validation failed", - "issues": [ - { - "slot": "json", - "issues": [ - { - "message": "Invalid input: expected string, received number", - "path": "name" - } - ] - } - ] } -``` - -When the returned value fails validation, the response is `500 Internal Server Error` with `{ "error": "Response validation failed" }` and without the issues. - -Both are thrown as an [`HTTPException`](/docs/api/exception), so you can customize the response in `app.onError()`. The raw issues are in `c.error.cause.issues`. - -```ts -app.onError((c) => { - const err = c.error - if (err instanceof HTTPException && err.status === 400) { - console.log(err.cause) // { issues: [{ slot: 'json', issues: [...] }] } - return c.json({ message: 'Bad Request' }, 400) - } - return c.text('Internal Server Error', 500) -}) -``` - -Instead of a schema, you can pass a function. It receives the value and the Context, and the value it returns is used as the validated value. If it returns a `Response`, that response is sent and the handler does not run. -```ts -app.get( - '/search', - defineHandler({ - query: (value, c) => { - const q = value.q - if (typeof q !== 'string') { - return c.text('q is required', 400) - } - return { q } - }, - })((c, { query }) => ({ results: search(query.q) })) -) +const factory = createFactory() ``` -Middleware can be passed before the handler here too. It runs before the validation. +### Options -```ts -app.post( - '/users', - defineHandler({ json: UserSchema })(auth, (c, { json }) => { - return createUser(c.get('user'), json) - }) -) -``` +### defaultAppOptions: `HonoOptions` -The validated input and the response are inferred by the [RPC](/docs/guides/rpc) client. With `response`, the response type is the schema's output, without the failure responses. +The default options to pass to the Hono application created by `createApp()`. ```ts -const client = hc('http://localhost') - -const res = await client.users[':id'].$post({ - param: { id: '1' }, - json: { name: 'Hono' }, +const factory = createFactory({ + defaultAppOptions: { strict: false }, }) -const user = await res.json() // { id: string; name: string } -``` - -### Typing the Context -Without middleware, pass the `Env` and the path as type arguments. - -```ts -type Env = { - Variables: { - user: User - } -} - -app.get( - '/users/:id', - defineHandler((c) => { - c.get('user') // `User` - c.req.param('id') // `string` - return c.get('user') - }) -) +const app = factory.createApp() // `strict: false` is applied ``` -To set the `Env` only once, use [`factory.defineHandler()`](#factory-definehandler). - -## `defineMiddleware()` +## `createMiddleware()` -`defineMiddleware()` defines a middleware with the types. +`createMiddleware()` is shortcut of `factory.createMiddleware()`. +This function will create your custom middleware. ```ts -import { defineMiddleware } from 'hono/factory' - -const messageMiddleware = defineMiddleware(async (c, next) => { +const messageMiddleware = createMiddleware(async (c, next) => { await next() c.res.headers.set('X-Message', 'Good morning!') }) @@ -212,7 +61,7 @@ Tip: If you want to get an argument like `message`, you can create it as a funct ```ts const messageMiddleware = (message: string) => { - return defineMiddleware(async (c, next) => { + return createMiddleware(async (c, next) => { await next() c.res.headers.set('X-Message', message) }) @@ -221,67 +70,28 @@ const messageMiddleware = (message: string) => { app.use(messageMiddleware('Good evening!')) ``` -## `createFactory()` +## `factory.createHandlers()` -`createFactory()` will create an instance of the Factory class. +`createHandlers()` helps to define handlers in a different place than `app.get('/')`. ```ts import { createFactory } from 'hono/factory' +import { logger } from 'hono/logger' -const factory = createFactory() -``` - -You can pass your Env types as Generics: - -```ts -type Env = { - Variables: { - foo: string - } -} - -const factory = createFactory() -``` - -### Options - -### defaultAppOptions: `HonoOptions` +// ... -The default options to pass to the Hono application created by `createApp()`. +const factory = createFactory() -```ts -const factory = createFactory({ - defaultAppOptions: { strict: false }, +const middleware = factory.createMiddleware(async (c, next) => { + c.set('foo', 'bar') + await next() }) -const app = factory.createApp() // `strict: false` is applied -``` - -## `factory.defineHandler()` - -`factory.defineHandler()` is `defineHandler()` with the `Env` of the factory. Everything in [`defineHandler()`](#definehandler) applies. - -```ts -const factory = createFactory() - -const getUser = factory.defineHandler((c) => { - return c.get('foo') // `string` +const handlers = factory.createHandlers(logger(), middleware, (c) => { + return c.json(c.var.foo) }) -app.get('/users/:id', getUser) -``` - -## `factory.defineMiddleware()` - -`factory.defineMiddleware()` is `defineMiddleware()` with the `Env` of the factory. - -```ts -const factory = createFactory() - -const middleware = factory.defineMiddleware(async (c, next) => { - c.set('foo', 'bar') - await next() -}) +app.get('/api', ...handlers) ``` ## `factory.createApp()` @@ -291,7 +101,7 @@ const middleware = factory.defineMiddleware(async (c, next) => { If your application is like this, you have to set the `Env` in two places: ```ts -import { defineMiddleware } from 'hono/factory' +import { createMiddleware } from 'hono/factory' type Env = { Variables: { @@ -302,8 +112,8 @@ type Env = { // 1. Set the `Env` to `new Hono()` const app = new Hono() -// 2. Set the `Env` to `defineMiddleware()` -const mw = defineMiddleware(async (c, next) => { +// 2. Set the `Env` to `createMiddleware()` +const mw = createMiddleware(async (c, next) => { await next() }) @@ -322,8 +132,8 @@ const factory = createFactory() const app = factory.createApp() -// factory also has `defineMiddleware()` -const mw = factory.defineMiddleware(async (c, next) => { +// factory also has `createMiddleware()` +const mw = factory.createMiddleware(async (c, next) => { await next() }) ``` @@ -363,37 +173,3 @@ app.post('/posts', (c) => { // ... }) ``` - -## `createMiddleware()` - -**`createMiddleware()` is deprecated**. Use [`defineMiddleware()`](#definemiddleware) instead. It is the same function with a new name, and `factory.createMiddleware()` is `factory.defineMiddleware()`. - -```ts -// Before -const mw = createMiddleware(async (c, next) => { - await next() -}) - -// After -const mw = defineMiddleware(async (c, next) => { - await next() -}) -``` - -## `factory.createHandlers()` - -**`factory.createHandlers()` is deprecated**. Use [`defineHandler()`](#definehandler) instead, and pass the middleware before the handler. - -```ts -// Before -const handlers = factory.createHandlers(logger(), middleware, (c) => { - return c.json(c.var.foo) -}) -app.get('/api', ...handlers) - -// After -const handler = factory.defineHandler(logger(), middleware, (c) => { - return c.json(c.var.foo) -}) -app.get('/api', handler) -``` diff --git a/docs/index.md b/docs/index.md index 2adaae91..b03f38dd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,15 +47,13 @@ deno init --npm hono@latest ::: -The project comes with an `AGENTS.md` and [Hono CLI](/docs/agents/cli), so your coding agent is ready to go. See [Hono for Coding Agents](/docs/agents/). - ## Features - **Ultrafast** 🚀 - The router `RegExpRouter` is really fast. Not using linear loops. Fast. - **Lightweight** ðŸŠķ - The `hono/tiny` preset is under 14kB. Hono has zero dependencies and uses only the Web Standards. - **Multi-runtime** 🌍 - Works on Cloudflare Workers, Fastly Compute, Deno, Bun, AWS Lambda, or Node.js. The same code runs on all platforms. - **Batteries Included** 🔋 - Hono has built-in middleware, custom middleware, third-party middleware, and helpers. Batteries included. -- **Delightful DX** 😃 - Super clean APIs and first-class TypeScript support. Create a project with create-hono and your coding agent is ready to go. +- **Delightful DX** 😃 - Super clean APIs. First-class TypeScript support. Now, we've got "Types". ## Use-cases diff --git a/index.md b/index.md index db6e0159..8be3cc36 100644 --- a/index.md +++ b/index.md @@ -33,5 +33,5 @@ features: details: Hono has built-in middleware, custom middleware, third-party middleware, and helpers. Batteries included. - icon: 😃 title: Delightful DX - details: Super clean APIs and first-class TypeScript support. Create a project with create-hono and your coding agent is ready to go. + details: Super clean APIs. First-class TypeScript support. Now, we've got "Types". ---