多年来,选择 JavaScript Web 框架意味着接受权衡:Express 通用但慢且绑定 Node,Fastify 快但只能用于 Node,Next.js 功能完整但沉重。当边缘运行时——Cloudflare Workers、Deno Deploy、Bun、Vercel Edge——出现时,这些框架显露出局限:依赖不兼容、bundle 巨大、API 绑定 Node 的 req/res。
Hono(日语意为"火焰" 🔥)是现代答案。一个不到 14KB 的框架,完全基于 Web Standards(Request、Response、fetch),在任何 JavaScript 运行时上都能运行。同一份代码可部署到 Cloudflare Workers、Bun、Deno、Node.js、Vercel、Netlify 和 AWS Lambda——无需修改。
为什么选 Hono
Hono 在三件事上比所有人做得都好:
- 性能。
RegExpRouter将所有路由模式编译成单个正则表达式,避免传统路由器的线性循环。基准测试超过每秒 40 万次操作,使 Hono 跻身 JavaScript 生态系统中最快的路由器之列。 - 可移植性。 Web Standards 意味着零 Node 依赖。同一个
app.fetch在 Cloudflare Worker 中作为默认导出、传递给Bun.serve、挂载在 Deno 服务器上,或通过@hono/node-server适配。 - TypeScript 优先的 DX。 路径参数被推断为字面量类型,端到端类型安全的 RPC 客户端,能推断输入输出类型的验证器。自动补全几乎像心灵感应。
开始使用
最快的方式是官方启动器,它会为你选择的运行时搭建项目。
npm create hono@latest my-api cd my-api npm install npm run dev
启动器会询问使用哪个模板:cloudflare-workers、bun、deno、nodejs、vercel、aws-lambda、nextjs 等。它会选择合适的配置和部署脚本。要快速尝试,也可以从单个文件开始:
// src/index.ts import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hello Hono!')) export default app
在 Cloudflare Workers 上这就够了。在 Bun:Bun.serve({ fetch: app.fetch, port: 3000 })。在 Node:来自 @hono/node-server 的 serve({ fetch: app.fetch })。接口完全相同。
路由
路由用 HTTP 动词方法声明,支持参数、通配符和正则。
import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Home')) app.get('/posts/:id', (c) => { const id = c.req.param('id') // string,已类型化 return c.json({ id }) }) app.get('/posts/:id/comments/:commentId', (c) => { const { id, commentId } = c.req.param() return c.json({ id, commentId }) }) app.get('/files/*', (c) => c.text('Wildcard')) app.post('/posts', async (c) => { const body = await c.req.json() return c.json({ created: body }, 201) })
参数被推断为字面量类型:TypeScript 知道 c.req.param('id') 仅当你在模式中声明了 :id 时才返回 string。拼错名字是编译时错误。
分组路由
app.route() 允许将子应用作为模块组合,每个都有自己的前缀。
// routes/posts.ts import { Hono } from 'hono' const posts = new Hono() posts.get('/', (c) => c.json({ posts: [] })) posts.get('/:id', (c) => c.json({ id: c.req.param('id') })) export default posts // src/index.ts import { Hono } from 'hono' import posts from './routes/posts' const app = new Hono() app.route('/posts', posts)
嵌套路由继承根应用的 base path 和类型,所以 RPC 客户端能看到整个结构。
Context 对象
每个 handler 都接收一个 c——当前请求的 Context。这是你需要学习的唯一 API,用于读取输入和产生输出。
app.post('/echo', async (c) => { // 读取 const userAgent = c.req.header('User-Agent') const page = c.req.query('page') const body = await c.req.json() const env = c.env // bindings(Cloudflare 上的 KV、D1、secrets) // 在中间件和 handler 之间共享变量 c.set('requestId', crypto.randomUUID()) const id = c.get('requestId') // 响应 c.header('X-Request-Id', id) c.status(200) return c.json({ userAgent, page, body, id }) })
主要响应方法是 c.text、c.json、c.html、c.body(raw)和 c.redirect。所有方法都接受状态码作为第二个参数。
中间件:洋葱模型
中间件是 (c, next) => ... 函数,可以在 handler 前后运行代码。组合起来形成经典的洋葱模型:第一个注册的中间件最先开始,最后结束。
import { Hono } from 'hono' import { logger } from 'hono/logger' import { cors } from 'hono/cors' import { secureHeaders } from 'hono/secure-headers' const app = new Hono() app.use('*', logger()) app.use('*', secureHeaders()) app.use('/api/*', cors({ origin: 'https://spinny.dev' })) app.use('*', async (c, next) => { const start = performance.now() await next() c.header('X-Response-Time', `${performance.now() - start}ms`) })
await next() 将控制权交给下一个中间件。其后的代码在响应阶段运行,即在 handler 产生结果之后。在 next() 之前返回 Response 会短路链。
内置中间件
Hono 自带丰富的生产就绪中间件,可从 hono/... 导入:
| 中间件 | 用途 |
|---|---|
logger | 方法、路径、状态、时长的结构化日志 |
cors | 可按 origin、方法、headers 配置的 CORS |
csrf | 基于 origin 的 CSRF 防护 |
secureHeaders | 设置 CSP、HSTS、X-Frame-Options |
bearerAuth / basicAuth | 开箱即用的 Bearer/Basic 认证 |
jwt | 用 jose 验证和签名 JWT |
etag | 生成 ETag 并处理 304 |
cache | 通过 Web Cache API 缓存 |
compress | 响应 gzip/deflate |
bodyLimit | 拒绝超过阈值的 body |
timing | 用于性能分析的 Server-Timing 头 |
类型安全的自定义中间件
要用类型化变量扩展 Context,使用 createMiddleware:
import { createMiddleware } from 'hono/factory' type AuthVars = { userId: string; role: 'user' | 'admin' } export const requireAuth = createMiddleware<{ Variables: AuthVars }>( async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', '') if (!token) return c.json({ error: 'Unauthorized' }, 401) const payload = await verifyJwt(token) c.set('userId', payload.sub) c.set('role', payload.role) await next() } ) // 使用 app.get('/me', requireAuth, (c) => { const userId = c.var.userId // string,已类型化 return c.json({ userId }) })
中间件下游,c.var.userId 无需任何 cast 即被类型化。这会贯穿整个链。
用 Zod 验证
@hono/zod-validator 将 Zod 接入请求周期。定义 schema,应用到路由,得到已验证、已类型化的输入。
import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const createPost = z.object({ title: z.string().min(1).max(200), body: z.string().min(1), tags: z.array(z.string()).default([]), }) app.post( '/posts', zValidator('json', createPost), (c) => { const data = c.req.valid('json') // 按 schema 类型化 return c.json({ ok: true, post: data }, 201) } )
如果 body 验证失败,Hono 会在调用 handler 之前以 Zod 错误响应 400。也可以验证 query、param、header、cookie 和 form。
RPC:端到端类型安全的客户端
真正让 Hono 与众不同的特性是 RPC 模式。从服务器导出应用类型,hc 客户端导入它,你就获得完整的自动补全——包括路径、查询、body、headers 和响应——无需 codegen 或 OpenAPI。
// server.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const app = new Hono() .get('/posts/:id', (c) => c.json({ id: c.req.param('id'), title: 'Hello' }) ) .post( '/posts', zValidator('json', z.object({ title: z.string(), body: z.string() })), (c) => c.json({ ok: true }, 201) ) export type AppType = typeof app export default app
// client.ts import { hc } from 'hono/client' import type { AppType } from './server' const client = hc<AppType>('https://api.spinny.dev') const res = await client.posts[':id'].$get({ param: { id: '42' } }) if (res.ok) { const data = await res.json() // { id: string, title: string } console.log(data.title) } const created = await client.posts.$post({ json: { title: '你好', body: 'Hono 是火焰' }, // 已验证 })
在服务器上重命名一个路由,客户端的 TypeScript 立即在 CI 中失败。这与 tRPC 同样有优势,但基于标准 HTTP,没有特定中间件,bundle 极小。
状态码区分
如果返回不同状态,客户端会自动区分。
.get('/posts/:id', (c) => { const post = findPost(c.req.param('id')) if (!post) return c.json({ error: 'not found' }, 404) return c.json({ post }, 200) })
const res = await client.posts[':id'].$get({ param: { id } }) if (res.status === 404) { const { error } = await res.json() // { error: string } } if (res.status === 200) { const { post } = await res.json() // { post: Post } }
路由器与性能
Hono 提供五种具有不同权衡的路由器。默认是 SmartRouter,它在启动时测量哪个路由器最适合你的路由并锁定。
| 路由器 | 优势 | 何时使用 |
|---|---|---|
RegExpRouter | 速度最快,编译正则 | 大多数 API 的默认 |
TrieRouter | 支持所有模式 | RegExp 处理不了的复杂模式 |
SmartRouter | 自动选择最佳 | 推荐默认 |
LinearRouter | 注册超快 | One-shot worker,冷启动关键 |
PatternRouter | 最小 bundle(<15KB) | 极端尺寸限制 |
对于频繁冷启动的无状态 worker,LinearRouter 跳过初始编译成本,在测量注册加匹配时比 find-my-way 快 33 倍。
import { Hono } from 'hono' import { LinearRouter } from 'hono/router/linear-router' const app = new Hono({ router: new LinearRouter() })
多运行时部署
同一个 export default app 仅通过更换入口文件就能切换目标。
Cloudflare Workers
// src/index.ts import { Hono } from 'hono' type Bindings = { MY_KV: KVNamespace; DB: D1Database } const app = new Hono<{ Bindings: Bindings }>() app.get('/cache/:key', async (c) => { const value = await c.env.MY_KV.get(c.req.param('key')) return c.json({ value }) }) export default app
部署:npx wrangler deploy。KV/D1/R2/Queues 绑定原生支持。
Bun
import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Bun + Hono')) Bun.serve({ fetch: app.fetch, port: 3000 })
Node.js
import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Node + Hono')) serve({ fetch: app.fetch, port: 3000 })
Deno
import { Hono } from 'jsr:@hono/hono' const app = new Hono() app.get('/', (c) => c.text('Deno + Hono')) Deno.serve(app.fetch)
Vercel
// api/[[...route]].ts import { Hono } from 'hono' import { handle } from 'hono/vercel' const app = new Hono().basePath('/api') app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' })) export const GET = handle(app) export const POST = handle(app)
相同的业务代码、相同的路由、相同的中间件。仅适配器不同。
真实示例:带认证和数据库的 REST API
把所有部分组合起来。一个 Cloudflare Workers + D1 上的博客 API,带 JWT 认证、验证和 RPC。
import { Hono } from 'hono' import { jwt } from 'hono/jwt' import { logger } from 'hono/logger' import { cors } from 'hono/cors' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' type Bindings = { DB: D1Database; JWT_SECRET: string } type Variables = { jwtPayload: { sub: string } } const app = new Hono<{ Bindings: Bindings; Variables: Variables }>() app.use('*', logger()) app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true })) const auth = (c: any, next: any) => jwt({ secret: c.env.JWT_SECRET })(c, next) const api = app.basePath('/api') api.get('/posts', async (c) => { const { results } = await c.env.DB .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50') .all() return c.json({ posts: results }) }) api.post( '/posts', auth, zValidator('json', z.object({ title: z.string().min(1).max(200), body: z.string().min(1), })), async (c) => { const { title, body } = c.req.valid('json') const userId = c.var.jwtPayload.sub const result = await c.env.DB .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id') .bind(title, body, userId) .first<{ id: number }>() return c.json({ id: result?.id }, 201) } ) api.onError((err, c) => { console.error(err) return c.json({ error: 'Internal error' }, 500) }) export type AppType = typeof api export default app
React 客户端以完整的类型安全消费它:
import { hc } from 'hono/client' import type { AppType } from '../api/src/index' const api = hc<AppType>(import.meta.env.VITE_API_URL) const res = await api.posts.$post({ json: { title: '2026 年的 Hono', body: '...' }, }, { headers: { Authorization: `Bearer ${token}` }, })
测试
app.request() 让你无需启动 HTTP 服务器即可测试路由。这是与生产相同的路径,在内存中执行。
import { describe, it, expect } from 'vitest' import app from '../src/index' describe('GET /api/posts', () => { it('返回帖子列表', async () => { const res = await app.request('/api/posts') expect(res.status).toBe(200) const body = await res.json() expect(body.posts).toBeInstanceOf(Array) }) })
在 Cloudflare Workers 上,@cloudflare/vitest-pool-workers 在带模拟 binding 的真实 Worker 内运行相同的测试——最大真实性,零部署。
最佳实践
1. 链式定义路由
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
链式让 app 的类型在每条路由后保持最新,对 RPC 客户端至关重要。在不同行用 app.get(...) 单独定义会破坏推断。
2. 导出类型,而非实现
客户端必须导入 AppType,而不是 app。import type 确保前端 build 不包含后端代码。
3. 每个领域一个路由器
一个子应用用于 posts,一个用于 users,一个用于 webhooks。用 app.route() 组合,每个都拥有自己的中间件。结构无需巨型文件即可扩展。
4. 始终在边界验证
每个外部输入(body、query、header)都必须经过 zValidator。不要信任下游数据:即使没有运行时验证的 TypeScript cast 也是潜伏的 bug。
5. 依赖 binding,而非全局客户端
在 Cloudflare 上,通过 c.env 访问 KV/D1/R2。无全局单例,无跨 Worker 持久化的连接。无状态模型是特性,不是限制。
6. 优化路由器前先测量
默认的 SmartRouter 适用于 95% 的情况。仅在分析并发现真实瓶颈后才切换路由器。
结论
到 2026 年,Hono 已成为用 TypeScript 构建边缘就绪 API 的事实标准。Web Standards、性能、类型安全和可移植性的组合,恰好解决了束缚传统框架的问题:运行时锁定、沉重的 bundle、客户端与服务器之间脆弱的类型系统。
你将用它构建 Cloudflare Workers 上的微服务,替换 Node 单体中的 Express,构建 Vercel 上的函数,或 Bun 上的 API。同样的知识在所有地方都通用——这正是它在未来几年成为稳健投资的原因。
入门检查清单:
npm create hono@latest并选择你的运行时模板- 用链式定义路由(
.get(...).post(...))- 添加
logger、cors、secureHeaders作为全局中间件- 用
@hono/zod-validator验证每个输入- 导出
AppType并用类型安全的hc客户端消费 API- 用
app.request()编写测试——无需 HTTP 服务器- 用
wrangler deploy(CF)、vercel deploy或你运行时的 bundler 部署