# Thực hành: dựng lớp API mỏng bằng Hono trên Cloudflare Workers để bọc hệ thống cũ của khách

> Hệ thống của khách thường không sửa được, nhưng bạn vẫn có thể đặt trước nó một lớp API sạch, có kiểu và có xác thực mà không phải đụng vào một dòng code cũ nào.

Bản gốc: https://fdetimes.net/vi/bach-khoa/hono-lop-api-mong-boc-he-thong-khach-tren-cloudflare-workers/

Tuần đầu ở chỗ khách, bạn hay gặp cảnh này: dữ liệu mà agent hoặc dashboard của bạn cần nằm trong một hệ thống đơn hàng mười năm tuổi. Tên trường viết hoa toàn bộ, lỗi trả về dạng HTML, và không ai dám sửa code của nó. Bạn cũng không cần sửa. Việc của bạn là dựng một lớp mỏng đứng trước nó.

Đây đúng là loại việc mà các tin tuyển FDE ghi thẳng vào mô tả công việc. Tin tuyển Forward Deployed Engineer II của Synapse Analytics yêu cầu ứng viên dựng và bảo trì các thành phần tích hợp riêng cho từng khách, gồm adapter, connector, script và API, rồi biến những mẫu lặp lại thành cải tiến cho sản phẩm.

Bài này dựng một lớp như vậy bằng Hono và TypeScript trên Cloudflare Workers. Theo tài liệu của Cloudflare, Hono là framework nhẹ và chạy rất hợp với Workers. Phần code dưới đây được rút gọn cho dễ theo dõi, nên đối chiếu với tài liệu chính thức trước khi đưa cho khách dùng.

## Bạn sẽ dựng gì, và cần chuẩn bị gì?

Ví dụ giả định: khách có một API đơn hàng cũ nhận `ITEM_CODE` và `QUANTITY`. Lớp bọc của bạn mở ra endpoint `POST /api/orders` nhận JSON gọn gàng `{ sku, qty }`, kiểm tra token của bên gọi, kiểm tra payload, gọi hệ thống cũ bằng token riêng, và đổi mọi sự cố phía sau thành lỗi JSON thống nhất.

Bạn cần Node.js và npm, một tài khoản Cloudflare nếu muốn deploy, cùng URL và token của hệ thống cần bọc. Khi luyện tập ở nhà, bất kỳ API nội bộ nào bạn có quyền gọi đều dùng được.

## Bước 1: khung project và endpoint đầu tiên

Hono có lệnh `create-hono` để tạo project. Chạy lệnh dưới đây, rồi chọn template Cloudflare Workers khi được hỏi:

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

Entry point là `src/index.ts`, export app làm handler mặc định. Thêm một endpoint health check để chắc chắn mọi thứ đã chạy:

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

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

export default app
```

Chạy `npm run dev` rồi mở URL local mà Wrangler in ra. **Kiểm tra:** `/health` phải trả về `{"ok":true}`. Mở `package.json` để xem các script `dev` và `deploy` gọi Wrangler như thế nào. Hai lệnh này sẽ đi cùng bạn suốt dự án.

## Bước 2: token của khách nằm ở đâu?

Đây là chỗ nhiều người làm sai ngay từ đầu. Hướng dẫn best practices của Cloudflare nói rõ: API key, token và thông tin đăng nhập database không bao giờ được xuất hiện trong file cấu hình Wrangler hay trong source code. Hãy đưa chúng vào secret:

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

Khi chạy local bằng `npm run dev`, Wrangler đọc secret từ file `.dev.vars` ở thư mục gốc của project. Dùng token thử, không dùng token thật của khách, và thêm file này vào `.gitignore`:

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

Cloudflare cũng khuyên không tự viết interface `Env` mà sinh nó bằng `wrangler types`. Chạy lệnh này mỗi khi bạn thêm binding mới:

```bash
npx wrangler types
```

Sau đó sửa dòng `const app = new Hono()` ở bước 1 thành dòng dưới đây. Đây là thay thế, không phải khai báo thêm một `app` thứ hai:

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

**Kiểm tra:** gõ `c.env.` trong editor phải gợi ý đúng tên các biến. URL của hệ thống cũ (`UPSTREAM_URL`) không phải bí mật nên có thể để làm biến cấu hình thông thường.

## Bước 3: chặn request xấu trước khi nó chạm tới khách

Trước khi dịch bất cứ thứ gì, lớp bọc phải biết nói "không". Middleware Bearer Auth của Hono không bắt token phải là JWT, và bạn có thể tự kiểm tra token bằng một hàm `verifyToken`:

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

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

Tiếp theo là kiểm tra payload. Zod là thư viện validation thiết kế riêng cho TypeScript, và validator middleware của Hono kiểm tra được `json`, `query`, `header`, `param`, `cookie` và `form`. Cài gói rồi khai báo 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(),
})
```

**Kiểm tra:** sau khi gắn schema vào route ở bước 4, gửi request không có header `Authorization` thì phải nhận 401. Gửi `qty: -3` thì phải bị từ chối với lỗi 400 trước khi bất kỳ dòng nào phía sau kịp chạy.

## Bước 4: dịch sang ngôn ngữ của hệ thống cũ và gom lỗi

Bây giờ mới đến phần dịch. Handler nhận dữ liệu đã được kiểm tra, đổi tên trường cho khớp với hệ thống cũ và gọi sang đó bằng token riêng:

```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: 'Order system unavailable' })
  return c.json(await res.json(), 201)
})
```

Khi hệ thống cũ trả về trang lỗi HTML, bên gọi không nên phải nhìn thấy nó. Hono cho phép ném `HTTPException` và xử lý tập trung bằng `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: 'Internal error' }, 500)
})
```

**Kiểm tra:** trỏ tạm `UPSTREAM_URL` sang một địa chỉ sai. Bên gọi phải nhận JSON có `error`, không bao giờ nhận stack trace hay HTML.

**Điểm mấu chốt:** Giá trị của một lớp bọc nằm ở những request mà nó từ chối, không chỉ ở những request nó chuyển đi.

## Bước 5: chia sẻ kiểu và deploy

Nếu chính bạn viết luôn phần code gọi API, chẳng hạn agent hay frontend, tính năng RPC của Hono giúp server và client dùng chung đặc tả API. Bạn chỉ cần export kiểu của app:

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

Bên client import `AppType` và có ngay một client đã gắn kiểu. Đổi tên trường ở server là client báo lỗi lúc biên dịch, thay vì báo lỗi ở chỗ khách. Cloudflare cũng ghi chú rằng có thể ghép API Hono với một SPA qua Workers Assets nếu sau này bạn cần thêm giao diện.

Cuối cùng, chạy `npm run deploy`. Worker có thể chạy trên subdomain `*.workers.dev` hoặc trên một custom domain, và deploy được từ laptop lẫn từ bất kỳ hệ thống CI/CD nào. Ở chỗ khách, nên đưa lệnh deploy vào CI ngay từ tuần đầu để không ai phụ thuộc vào máy của bạn.

## Những lỗi khiến lớp bọc gãy ở production

Lỗi khó thấy nhất: Workers tái sử dụng isolate giữa các request. Nếu bạn lưu token hay dữ liệu của một request vào biến cấp module, request sau có thể đọc được dữ liệu của request trước. Mọi thứ thuộc về một request phải nằm trong handler, lấy từ `c`.

Lỗi tiếp theo là bỏ qua giới hạn của gói dịch vụ. Gói Free cho 10 ms CPU mỗi request và 50 subrequest mỗi lần gọi. Thử hình dung một endpoint "lấy chi tiết đơn" phải gọi hệ thống cũ một lần cho mỗi dòng hàng: đơn có 60 dòng là vượt ngưỡng ngay. Hãy gom lời gọi lại hoặc xem lại gói dịch vụ trước khi hứa với khách.

Cuối cùng là deploy mà không có cách nào nhìn vào bên trong. Cloudflare gọi một Worker production không có observability là một hộp đen. Bật nó trong file cấu hình Wrangler (ví dụ rút gọn dưới đây cho `wrangler.jsonc`; nếu template của bạn dùng `wrangler.toml` thì khai báo tương đương), rồi deploy lại:

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

Làm việc này trước khi lần đầu tiên có người hỏi bạn vì sao đơn hàng lúc 2 giờ sáng bị lỗi.

## Bắt đầu ở chỗ khách và ghi vào CV ra sao?

Ở chỗ khách, việc đầu tiên không phải là gõ code mà là liệt kê ba thứ: endpoint nào thật sự cần, ai sẽ gọi, và hệ thống cũ báo lỗi ra sao. Sau đó, từ một endpoint đọc và một endpoint ghi, bạn đã có thứ để demo ngay trong tuần.

Khi viết CV, đừng ghi chung chung là "xây dựng API". Hãy ghi bạn đã bọc hệ thống gì, dùng cách xác thực nào, chặn loại payload sai nào, và ánh xạ lỗi phía sau ra sao.

Đó chính là ngôn ngữ của mục "adapter, connector, API" trong tin tuyển FDE. Còn khi bạn bọc hệ thống thứ ba mà vẫn thấy lặp lại cùng một mẫu, đó là lúc kể cho đội sản phẩm nghe.

**Thử ngay tuần này:**

- Chọn một API nội bộ cũ mà bạn biết rõ, bọc một endpoint đọc và một endpoint ghi của nó bằng Hono, rồi deploy lên workers.dev.
- Viết schema Zod cho payload ghi và thử gửi 5 payload sai để xem lớp bọc chặn có đúng không.
- Ghi vào CV một dòng mô tả adapter đã dựng: hệ thống nguồn, số endpoint, cách xác thực và cách ánh xạ lỗi.

## Nguồn

- [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/)
