用 Hono.js + TypeScript 构建边缘 REST API — Cloudflare Workers 实战指南
我用 Hono v4、Bun 1.3 和 Zod v4 实际构建了一个类型安全的 REST API。本文涵盖路由定义、Zod 输入验证、CORS/日志/计时中间件,以及 Cloudflare Workers 部署配置——包含真实终端日志和分步骤代码示例。
用 Express 写过 REST API 的人,大概都有过这种感受:中间件注册、类型定义、body parser 配置、Joi 或 Zod 的接入……结构本身并不复杂,但样板代码多得烦人。第一次看到 Hono 时,我是持怀疑态度的,心想”又一个 Express 克隆吧”。直到真正用过之后,才改变了看法。
结论先说:Hono v4 不只是轻量和快速。TypeScript 类型推断能自然地流传到路由处理器;Zod 验证通过一个官方包就能接入;在 Bun 上运行时,响应速度对比 Express 有明显差距。本文内容来自我 2026 年 6 月在沙箱中实际运行的结果。
为什么选 Hono: 对比 Express 和 Fastify
要理解 Hono 的定位,需要回答三个问题。
包体积:Hono v4 核心约 12KB,Express 是 58KB,Fastify 是 77KB。数字差距看起来不大,但在 Cloudflare Workers 或 Deno Deploy 这类边缘环境中,包体积直接影响冷启动时间。边缘函数有时每个请求都会初始化新的运行时,所以越小,首次响应越快。
运行时兼容性:Express 只支持 Node.js。Fastify 也基本以 Node.js 为主要目标。而 Hono 从设计之初就把”到处都能运行”作为目标。同一份代码可以部署到 Bun、Deno、Cloudflare Workers、Node.js 和 AWS Lambda Edge。
TypeScript 支持:Express 需要单独安装 @types/express,中间件给 req 添加的属性也无法获得类型推断。Hono 从一开始就用 TypeScript 编写,Hono<{ Bindings: Env; Variables: Variables }> 泛型可以让环境变量和中间件状态也能类型安全地访问。
我并不是说 Hono 适合所有场景。如果团队已经深度使用 Express,或者需要成熟的插件生态,没有充分理由去切换。但如果目标是边缘部署,或者想从一开始就保证类型安全,Hono 是目前 TypeScript API 框架中最有说服力的选择。
安装与第一个服务器: 30 秒内收到响应
我在沙箱里从零开始。Bun 1.3.14 环境。
# 初始化新项目
bun init -y
# 安装 Hono v4
bun add hono
# 添加 Zod 验证包
bun add zod @hono/zod-validator
安装输出:
bun add v1.3.14 (0d9b296a)
installed hono@4.12.23
installed @hono/zod-validator@0.8.0
installed zod@4.4.3
安装时间不到 500ms。Hono 的依赖链几乎为空。
最简单的服务器:
// index.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.json({ message: 'Hello from Hono!' }))
export default app
bun run index.ts
# Started development server: http://localhost:3000
curl http://localhost:3000/
# {"message":"Hello from Hono!"}
export default app 这一行,同时被 Bun、Deno 和 Cloudflare Workers 识别为入口点。在 Node.js 中只需添加 serve(app) 即可。不需要针对不同运行时写分支代码,这是体感上最大的改善。
中间件栈: logger、CORS、timing

Hono 的内置中间件通过 hono/middleware-name 格式按需导入,只加载需要的部分,不会有多余代码进入包。
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { timing } from 'hono/timing'
const app = new Hono()
// 全局中间件,注册顺序即执行顺序
app.use('*', logger())
app.use('*', cors())
app.use('*', timing())
开启 logger() 后,每个请求会打印:
<-- GET /tasks
--> GET /tasks 200 0ms
我实际运行时,速度差异非常明显。第一个请求 3ms,后续请求服务端处理 0ms(亚毫秒级)。使用 timing() 后,响应头会携带 Server-Timing,在 Chrome DevTools Network 面板可以看到每个处理阶段的耗时。
CORS 可以精细配置:
app.use('*', cors({
origin: ['https://jangwook.net', 'http://localhost:5173'],
allowMethods: ['GET', 'POST', 'PATCH', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
}))
cors() 的默认行为是允许所有来源。生产环境中必须明确指定 origin。
Zod 输入验证: 自动返回 400 错误
@hono/zod-validator 是 Hono 的官方 Zod 集成包。作为中间件挂载到路由上后,Zod schema 验证失败时会自动返回 400 响应。
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const createTaskSchema = z.object({
title: z.string().min(1, '标题为必填项').max(100, '最多 100 个字符'),
completed: z.boolean().optional().default(false),
})
app.post('/tasks', zValidator('json', createTaskSchema), (c) => {
const body = c.req.valid('json')
// body 的类型被推断为 z.infer<typeof createTaskSchema>
// body.title 是 string,body.completed 是 boolean——不会有 undefined
const task = { id: nextId++, ...body, createdAt: new Date().toISOString() }
tasks.push(task)
return c.json({ data: task }, 201)
})
用空 title 测试的结果:
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title":""}'
{
"success": false,
"error": {
"name": "ZodError",
"message": "[{\"code\":\"too_small\",\"minimum\":1,\"path\":[\"title\"],\"message\":\"标题为必填项\"}]"
}
}
HTTP 400 自动返回,处理器内部不需要写验证代码。
c.req.valid('json') 是关键。返回的数据已经经过 Zod 验证并且是完全类型化的。如果你用过 Zod v4 和 Claude API 结构化输出的组合,v4 的 schema API 变更在这里同样适用。好在 @hono/zod-validator 同时支持 v3 和 v4,版本问题基本不用操心。
完整 CRUD 实现: 附实际执行日志
下面是完整的 Task CRUD API,包含实际运行的终端输出。用了内存存储(生产环境中替换为 D1、Prisma 或 Drizzle)。
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { timing } from 'hono/timing'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
app.use('*', logger())
app.use('*', cors())
app.use('*', timing())
interface Task {
id: number
title: string
completed: boolean
createdAt: string
}
let tasks: Task[] = [
{ id: 1, title: '安装 Hono', completed: true, createdAt: new Date().toISOString() },
{ id: 2, title: '构建 REST API', completed: false, createdAt: new Date().toISOString() },
]
let nextId = 3
const createTaskSchema = z.object({
title: z.string().min(1, '标题为必填项').max(100),
completed: z.boolean().optional().default(false),
})
const updateTaskSchema = z.object({
title: z.string().min(1).max(100).optional(),
completed: z.boolean().optional(),
})
app.get('/', (c) => c.json({ name: 'Task API', version: '1.0.0', runtime: 'Bun + Hono' }))
app.get('/tasks', (c) => {
const completedParam = c.req.query('completed')
let result = tasks
if (completedParam !== undefined) {
result = tasks.filter(t => t.completed === (completedParam === 'true'))
}
return c.json({ data: result, total: result.length })
})
app.post('/tasks', zValidator('json', createTaskSchema), (c) => {
const body = c.req.valid('json')
const task: Task = { id: nextId++, ...body, createdAt: new Date().toISOString() }
tasks.push(task)
return c.json({ data: task }, 201)
})
app.get('/tasks/:id', (c) => {
const id = parseInt(c.req.param('id'))
const task = tasks.find(t => t.id === id)
if (!task) return c.json({ error: '任务不存在' }, 404)
return c.json({ data: task })
})
app.patch('/tasks/:id', zValidator('json', updateTaskSchema), (c) => {
const id = parseInt(c.req.param('id'))
const body = c.req.valid('json')
const index = tasks.findIndex(t => t.id === id)
if (index === -1) return c.json({ error: '任务不存在' }, 404)
tasks[index] = { ...tasks[index], ...body }
return c.json({ data: tasks[index] })
})
app.delete('/tasks/:id', (c) => {
const id = parseInt(c.req.param('id'))
const index = tasks.findIndex(t => t.id === id)
if (index === -1) return c.json({ error: '任务不存在' }, 404)
tasks.splice(index, 1)
return c.json({ message: '删除成功' })
})
export default app
实际终端输出:
$ bun run index.ts
Started development server: http://localhost:3000
<-- GET /
--> GET / 200 4ms
<-- GET /tasks
--> GET /tasks 200 2ms
<-- POST /tasks
--> POST /tasks 201 4ms
<-- GET /tasks/3
--> GET /tasks/3 200 0ms
<-- PATCH /tasks/2
--> PATCH /tasks/2 200 0ms
<-- DELETE /tasks/1
--> DELETE /tasks/1 200 0ms
<-- POST /tasks (空标题)
--> POST /tasks 400 0ms
性能数据:首次请求 4ms,热状态下亚毫秒(日志显示 0ms)。同一台机器上运行 Express 时,热状态也需要 1〜2ms。在真实的生产边缘环境中,差距可能更大。
部署到 Cloudflare Workers: 几乎不改代码
Hono 的最大优势之一:换部署目标,代码几乎不用改。
bun add -g wrangler
# wrangler.toml
name = "hono-task-api"
main = "src/worker.ts"
compatibility_date = "2024-09-23"
[vars]
ENVIRONMENT = "production"
将 Cloudflare Workers 环境变量类型连接到 Hono:
// src/worker.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
type Bindings = {
ENVIRONMENT: string
DB: D1Database
KV: KVNamespace
}
type Variables = {
userId: string
}
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
app.use('*', cors())
app.get('/health', (c) => {
return c.json({
env: c.env.ENVIRONMENT, // 类型安全:string
timestamp: new Date().toISOString()
})
})
export default app
# 本地模拟 Cloudflare Workers 环境
wrangler dev
# 生产部署
wrangler deploy
我没有验证 wrangler deploy——这需要真实的 Cloudflare 账户来配置绑定。但代码结构如上所示,与本地 Bun 服务器唯一的区别是如何访问 c.env.DB 这类绑定。
查看 Cloudflare Workers 智能体基础设施,可以看到 Hono 已经在 Cloudflare 生态中作为 AI 智能体的 API 层被广泛使用。
类型安全的中间件: Variables 的用法
Express 中要对 req.user 实现类型安全,需要扩展接口。Hono 用 Variables 泛型处理得更干净。
type Variables = {
userId: string
requestId: string
}
const app = new Hono<{ Variables: Variables }>()
// 认证中间件
app.use('/tasks/*', async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401)
}
c.set('userId', 'user-123')
c.set('requestId', crypto.randomUUID())
await next()
})
// 路由处理器中类型安全地访问
app.get('/tasks', (c) => {
const userId = c.get('userId') // 推断为 string
const requestId = c.get('requestId') // 推断为 string
return c.json({ userId, requestId })
})
我觉得不足的地方
实际用下来,也有值得说的限制。
生态系统深度:Fastify 的插件生态经过充分验证。fastify-swagger 可以自动生成 OpenAPI 规范,fastify-multipart 处理文件上传,这些都是经过验证和维护的插件。Hono 的第三方生态目前较薄。官方中间件覆盖了大部分基础需求,但遇到特殊需求时,可能需要自己实现。
D1 本地开发体验:在本地测试 Cloudflare D1 需要 wrangler dev,而这又需要真实的 Cloudflare 账户来配置绑定。基于 SQLite 兼容性,Drizzle 和 Prisma 都可以使用,但本地开发环境的搭建比 Express + PostgreSQL 的组合要复杂。
如果边缘部署不是你的目标,只是搭建普通服务器,Fastify 比 Hono 更成熟。像 Ollama + FastAPI 的搭配那样换个语言和运行时,也是实际可行的路线。
什么时候该选 Hono
我的判断总结如下。
应该选 Hono 的情况:
- 部署目标是 Cloudflare Workers、Deno Deploy 或 Bun 等边缘/无服务器环境
- 想从第一行代码起就保证 TypeScript 类型安全
- 包体积和冷启动时间对服务性能有直接影响
- 团队较小,希望用最少的样板代码快速启动
不必切换的情况:
- 团队已习惯 Express 或 Fastify,且没有边缘部署计划
- 大型企业服务需要成熟的插件生态
- 与大量遗留 Node.js 代码库需要深度集成
2026 年,Hono 的 GitHub star 数已超过 66,000。如果你已经搭建了 Bun Shell 脚本环境,加入 Hono 是自然的下一步——同一个运行时、同一个包管理器、同一个 TypeScript 生态,API 服务器也包含进来了。
Hono vs Express: 什么时候用,什么时候避开
在实际用过两个框架之后,我整理出了这套决策依据。不是抽象的”更快”,而是可以套到真实项目上的表格。
| 场景 | 推荐 Hono | 推荐 Express |
|---|---|---|
| 部署目标是 Cloudflare Workers / Deno Deploy / Bun | ✓ | |
| 冷启动和包体积直接影响延迟 | ✓ | |
| 想从一开始就把 TypeScript 类型推断贯穿到底 | ✓ | |
| 团队已经深度依赖 Express 中间件生态 | ✓ | |
passport、multer 等久经验证的 Express 插件是核心 | ✓ | |
| 需要在遗留 Node.js 代码库上逐步叠加 | ✓ | |
| OpenAPI 自动文档化是硬性要求 | ✓ (@hono/zod-openapi) | ✓ (swagger-jsdoc) |
应该避开 Hono 的现实情况有两种。第一,整个团队都熟悉 Express,且完全没有边缘部署计划。这时更换框架的成本超过收益。第二,被某个 Express 专属插件强绑定。Hono 基于标准的 Request/Response,无法直接搬用 Express 中间件。
反过来也很清楚。如果是新项目、部署目标是 serverless、TypeScript 是默认语言,那就很难找到选 Express 的理由。照着官方 Getting Started 一个页面走一遍,就能跑出一个可用的服务器。
参考的一手来源
本文的代码和数值都是直接照着以下官方文档验证的。
- Hono 官方文档 — Getting Started: 基本应用结构、路由、各运行时的入口点
- Hono 官方文档 — Cloudflare Workers: Bindings 泛型与
wrangler部署流程 - Cloudflare Workers 官方指南 — Hono: 在 Workers 运行时下的 Hono 项目配置与部署
- Zod 官方文档: 模式定义与类型推断,
@hono/zod-validator所依赖的校验规则
各文档版本以 2026 年 6 月为准,遵循 Hono v4 系列 API。
常用模式速查表
// 读取查询参数
const page = c.req.query('page') ?? '1'
const limit = parseInt(c.req.query('limit') ?? '10')
// 读取路径参数
const id = c.req.param('id')
// 读取请求头
const auth = c.req.header('Authorization')
// JSON 响应(带状态码)
return c.json({ data: result }, 201)
// 文本响应
return c.text('OK')
// 重定向
return c.redirect('/new-path', 301)
// 流式响应
return c.stream(async (stream) => {
for (const chunk of chunks) {
await stream.write(chunk)
await stream.sleep(100)
}
})
// Cloudflare Workers 环境变量
const dbUrl = c.env.DATABASE_URL
// 路由分组
const api = new Hono()
api.get('/users', ...)
api.post('/users', ...)
app.route('/api/v1', api)
总结: 跑完之后留下的笔记
这篇文章从 bun add hono @hono/zod-validator zod 这一行开始,到实际跑通完整 CRUD API 结束。内存存储有局限,但路由、中间件和 Zod 验证的配合方式已经验证清楚了。
最让我印象深刻的是类型推断。c.req.valid('json') 拿到的数据直接就是 Zod schema 推断出的类型。c.set('userId', ...) 存的数据,从 c.get('userId') 取出来就是 string。经过中间件链,TypeScript 不会丢失类型信息。
我不会说继续用 Express 就没有理由了。但如果你正在用 TypeScript 和 Bun 启动一个新项目,同时考虑边缘部署,Hono 现在就值得采用。
测试环境
- Bun: 1.3.14
- hono: 4.12.23
- @hono/zod-validator: 0.8.0
- zod: 4.4.3
- typescript: 5.9.3
- macOS 15.x (Apple Silicon)
常见问题
用 Hono 构建边缘 REST API 的基本流程是什么?
Hono 和 Express 有什么区别?
如何部署到 Cloudflare Workers?
Zod 输入验证是如何处理的?
阅读其他语言版本
- 🇰🇷 한국어
- 🇯🇵 日本語
- 🇺🇸 English
- 🇨🇳 中文(当前页面)
这篇文章有帮助吗?
您的支持能帮助我创作更好的内容。请我喝杯咖啡吧。