# 实战：用 Hono 在 Cloudflare Workers 上搭一层轻量 API，封装客户的遗留系统

> 客户的系统通常改不了。但你仍然可以在它前面加一层干净、带类型、有认证的 API，旧代码一行都不用动。

原文: https://fdetimes.net/zh/guides/hono-cloudflare-workers-api-wrapper/

到客户现场的第一周，这样的场景很常见：你的智能体或看板需要的数据，存放在一套用了十年的订单系统里。字段名全是大写，报错返回的是 HTML，没人敢动它的代码。你也不必去动。你要做的，是在它前面搭一层轻量的封装。

这正是前线部署工程师（FDE）招聘启事里写明的工作。Synapse Analytics 招聘 Forward Deployed Engineer II 的启事要求候选人构建并维护面向具体客户的集成组件，包括适配器、连接器、脚本和 API，并把反复出现的模式沉淀为产品改进。

本文用 Hono 和 TypeScript 在 Cloudflare Workers 上搭建这样一层。根据 Cloudflare 的文档，Hono 是一个轻量框架，在 Workers 上运行效果很好。为了便于阅读，下面的代码做了精简，交付给客户之前请对照官方文档核实。

## 要做什么，需要准备什么

举一个假设的例子：客户有一个遗留订单 API，接收 `ITEM_CODE` 和 `QUANTITY`。你的封装层对外提供 `POST /api/orders` 接口，接收干净的 JSON：`{ sku, qty }`。它检查调用方的 token，校验载荷，用自己的 token 调用遗留系统，并把下游的每一种失败都转换成统一的 JSON 错误。

你需要 Node.js 和 npm；如果要部署，还需要一个 Cloudflare 账号；以及被封装系统的 URL 和 token。在家练习的话，任何你有权调用的内部 API 都可以。

## 第 1 步：项目骨架和第一个接口

Hono 提供 `create-hono` 命令来生成项目骨架。运行下面的命令，出现提示时选择 Cloudflare Workers 模板：

```bash
npm create hono@latest orders-wrapper
cd orders-wrapper
```

入口文件是 `src/index.ts`，它把 app 作为默认处理器导出。加一个健康检查接口，确认一切正常运行：

```ts
import { Hono } from 'hono'

const app = new Hono()
app.get('/health', (c) => c.json({ ok: true }))

export default app
```

运行 `npm run dev`，打开 Wrangler 打印出的本地 URL。**检查：**`/health` 必须返回 `{"ok":true}`。打开 `package.json`，看看 `dev` 和 `deploy` 两个脚本是怎样调用 Wrangler 的。整个项目都会用到这两条命令。

## 第 2 步：客户的 token 放在哪里？

很多人一开始就在这里出错。Cloudflare 的最佳实践指南说得很明确：API 密钥、token 和数据库凭据绝不能出现在 Wrangler 配置文件或源代码里。要把它们放进 secret：

```bash
npx wrangler secret put UPSTREAM_TOKEN
npx wrangler secret put CLIENT_TOKEN
```

用 `npm run dev` 在本地运行时，Wrangler 会从项目根目录的 `.dev.vars` 文件读取 secret。这里用测试 token，不要用客户的真实 token，并把这个文件加进 `.gitignore`：

```bash
UPSTREAM_TOKEN=dev-upstream-token
CLIENT_TOKEN=dev-client-token
```

Cloudflare 还建议不要手写 `Env` 接口，而是用 `wrangler types` 生成。每次新增绑定后都运行一次：

```bash
npx wrangler types
```

然后把第 1 步里的 `const app = new Hono()` 改成下面这一行。这是替换，不是再声明一个 `app`：

```ts
const app = new Hono<{ Bindings: Env }>()
```

**检查：**在编辑器里输入 `c.env.`，应当能提示出正确的变量名。遗留系统的 URL（`UPSTREAM_URL`）不是机密，可以作为普通配置变量。

## 第 3 步：在坏请求到达客户系统之前拦下它

在做任何转换之前，封装层得先学会说“不”。Hono 的 Bearer Auth 中间件不要求 token 必须是 JWT，你可以用 `verifyToken` 函数自己检查 token：

```ts
import { bearerAuth } from 'hono/bearer-auth'

app.use('/api/*', bearerAuth({
  verifyToken: async (token, c) => token === c.env.CLIENT_TOKEN,
}))
```

接下来是载荷校验。Zod 是为 TypeScript 设计的校验库，Hono 的 validator 中间件可以校验 `json`、`query`、`header`、`param`、`cookie` 和 `form`。安装依赖包并声明 schema：

```bash
npm i zod @hono/zod-validator
```

```ts
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const OrderInput = z.object({
  sku: z.string().min(1),
  qty: z.number().int().positive(),
})
```

**检查：**在第 4 步把 schema 挂到路由上之后，不带 `Authorization` 请求头的请求必须得到 401；发送 `qty: -3` 必须被 400 拒绝，下游代码一行都还没来得及执行。

## 第 4 步：翻译成遗留系统的语言，集中处理错误

到这一步才开始转换。处理器拿到已校验的数据，把字段改名以匹配遗留系统，再用遗留系统自己的 token 调用它：

```ts
import { HTTPException } from 'hono/http-exception'

const route = app.post('/api/orders', zValidator('json', OrderInput), async (c) => {
  const { sku, qty } = c.req.valid('json')
  const res = await fetch(`${c.env.UPSTREAM_URL}/createOrder`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${c.env.UPSTREAM_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ ITEM_CODE: sku, QUANTITY: qty }),
  })
  if (!res.ok) throw new HTTPException(502, { message: '订单系统不可用' })
  return c.json(await res.json(), 201)
})
```

遗留系统返回 HTML 错误页时，绝不能让调用方看到。Hono 允许你抛出 `HTTPException`，并用 `app.onError` 在一处统一处理错误：

```ts
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ error: err.message }, err.status)
  }
  console.error(err)
  return c.json({ error: '内部错误' }, 500)
})
```

**检查：**临时把 `UPSTREAM_URL` 指向一个错误地址。调用方必须收到带 `error` 字段的 JSON，绝不能是堆栈信息或 HTML。

**要点:** 封装层的价值在于它拒绝的请求，而不只是它放行的请求。

## 第 5 步：共享类型并部署

如果调用这个 API 的代码（比如智能体或前端）也由你来写，Hono 的 RPC 功能可以让服务端和客户端共享 API 规范。只需要导出 app 的类型：

```ts
export type AppType = typeof route
```

客户端导入 `AppType`，马上就能得到一个带类型的客户端。在服务端改了某个字段名，客户端会在编译时报错，而不是在客户现场出错。Cloudflare 还提到，如果之后需要用户界面，可以通过 Workers Assets 把 Hono API 和 SPA 组合在一起。

最后运行 `npm run deploy`。Worker 可以运行在 `*.workers.dev` 子域名或自定义域名上，可以从笔记本部署，也可以从任何 CI/CD 系统部署。在客户现场，第一周就把部署命令放进 CI，这样谁都不必依赖你的电脑。

## 让封装层在生产环境出问题的几个错误

最难发现的一个：Workers 会在多个请求之间复用 isolate。如果你把 token 或某次请求的数据存在模块级变量里，下一个请求可能会读到上一个请求的数据。凡是属于请求的东西，都必须放在处理器内部，并且从 `c` 获取。

其次是忽视套餐限制。Free 套餐每个请求允许 10 ms CPU 时间，每次调用最多 50 个子请求。设想一个“订单详情”接口，每个订单行都调用一次遗留系统：一个有 60 行的订单会直接超限。要么把调用合并成批量请求，要么在向客户承诺之前先核对套餐。

最后是部署之后什么都看不见。Cloudflare 把没有可观测性的生产 Worker 形容为一个黑盒。在 Wrangler 配置文件里打开它（下面是 `wrangler.jsonc` 的精简示例；如果你的模板用的是 `wrangler.toml`，请声明等价配置），然后重新部署：

```jsonc
{
  "observability": { "enabled": true }
}
```

一定要在第一次有人问你“凌晨 2 点那笔订单为什么失败”之前做好这件事。

## 在客户现场怎样起步，简历上怎样写

在客户现场，第一项任务不是写代码，而是列清三件事：真正需要哪些接口，谁来调用，遗留系统怎样报错。在此基础上，做出一个读接口和一个写接口，当周就有东西可以演示。

简历上不要笼统地写“构建了 API”。要写清楚封装的是哪套系统，用了哪种认证，拦截了哪几类坏载荷，下游错误是怎样映射的。

这正是 FDE 招聘启事里“适配器、连接器、API”那一条所用的语言。当你封装到第三套系统，又一次看到同样的模式时，就该去告诉产品团队了。

**本周动手试试:**

- 挑一个你熟悉的老旧内部 API，用 Hono 封装一个读接口和一个写接口，并部署到 workers.dev。
- 为写接口的载荷编写 Zod schema，发送 5 个坏载荷，检查封装层是否都正确拦截。
- 在简历上加一行，描述你构建的适配器：源系统、接口数量、认证方式以及错误映射方式。

## 信源

- [Cloudflare Workers - Hono](https://hono.dev/docs/getting-started/cloudflare-workers)

- [Hono · Cloudflare Workers docs](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/hono/)

- [Validation - Hono](https://hono.dev/docs/guides/validation)

- [Bearer Auth Middleware - Hono](https://hono.dev/docs/middleware/builtin/bearer-auth)

- [RPC - Hono](https://hono.dev/docs/guides/rpc)

- [Exception - Hono](https://hono.dev/docs/api/exception)

- [Limits · Cloudflare Workers docs](https://developers.cloudflare.com/workers/platform/limits/)

- [Workers best practices · Cloudflare Workers docs](https://developers.cloudflare.com/workers/best-practices/workers-best-practices/)

- [Forward Deployed Engineer II - Synapse Analytics](https://synapseanalytics.recruitee.com/o/forward-deployed-engineer-ii)

- [Zod](https://zod.dev/)
