Hono framework: Trải nghiệm thực chiến một web framework xây trên Web Standards
- Bài toán: Cần một lớp API chạy ở edge (Cloudflare Workers) nhẹ, cold start thấp, nhưng vẫn type-safe đầu-cuối mà không phải dựng OpenAPI/codegen.
- Giải pháp: Hono — framework xây trên Web Standards (Request/Response chuẩn), chạy đa runtime, middleware sẵn trong core, RPC chia sẻ type server→client.
- Kết quả: Bundle vài KB, cold start mili-giây, type chạy thẳng từ route ra client. Đổi lại phải kỷ luật với type "lạc quan" và biết khi nào KHÔNG dùng.
Câu trả lời ngắn: Hono là một web framework nhẹ (vài KB) xây trên Web Standards — dùng đúng Request/Response như fetch() trên trình duyệt — nên cùng một đoạn code chạy được trên Cloudflare Workers, Bun, Deno, Node và AWS Lambda. Đáng để một System Architect quan tâm không phải vì nó "nhanh hơn Express", mà vì nó bỏ được lớp ràng buộc vào runtime và cho type chạy thẳng từ route ra tới client.
TL;DR (Executive Summary)
- Bài toán: Lớp API của site này (và vài microservice nội bộ) chạy trên Cloudflare. Tôi cần thứ gì đó cold start ở mili-giây, bundle đủ nhỏ để deploy lên Workers, nhưng vẫn type-safe đầu-cuối mà không phải dựng OpenAPI rồi codegen.
- Giải pháp: Tôi viết lại lớp đó bằng Hono. Middleware (logger, CORS, JWT...) nằm sẵn trong core, không phải
npm installnăm package. Chế độ RPC chia sẻ type từ server sang client chỉ bằng một dòngexport type.- Kết quả: Bundle dưới ~15KB, cold start một con số mili-giây, đổi ngôn ngữ runtime gần như không phải sửa code. Cái giá phải trả: type của Hono "lạc quan" — nó tin bạn giữ middleware đúng thứ tự, nên kỷ luật phải tự mình giữ.
Định nghĩa lại: Hono là gì dưới góc nhìn Kiến trúc hệ thống?
Hầu hết bài giới thiệu gọi Hono là "Express nhanh hơn cho edge". Cách gọi đó dễ hiểu nhưng làm hỏng câu chuyện.
Khác biệt cốt lõi không nằm ở tốc độ, nó nằm ở lớp tương thích (compatibility layer). Express viết dựa trên req/res riêng của Node — một API có tuổi đời hơn một thập kỷ và chỉ tồn tại trong Node. Hono viết dựa trên Request/Response của Web Standards (WinterCG) — cùng một object bạn đã dùng khi gọi fetch() trong trình duyệt.
Hệ quả về mặt kiến trúc rất lớn: vì Hono chỉ nói "tiếng phổ thông" của runtime, nên cùng một handler chạy được trên Cloudflare Workers, Bun, Deno, Node, Vercel Edge, Fastly và Lambda mà gần như không sửa gì. Bạn không khóa kiến trúc vào một nhà cung cấp. Với một người phải nghĩ về vòng đời 3-5 năm của hệ thống, "không khóa vào runtime" là một tài sản, không phải một tính năng cho vui.
Vì sao tôi thử Hono (bối cảnh thật)
Site này deploy trên Cloudflare Pages, có vài serverless function (ví dụ một endpoint proxy gọi PageSpeed Insights để tránh lộ API key ra client). Khi số function tăng lên, hai vấn đề rất thật xuất hiện:
- Cold start và CPU budget. Workers tính phí theo CPU time và có giới hạn cứng. Một framework Node nặng nề mang lên đây hoặc không chạy, hoặc ăn cold start khó chịu. Tôi cần thứ khởi động gần như tức thì.
- Type bị đứt ở biên giới network. Frontend gọi function, nhưng kiểu dữ liệu request/response phải tự tay đồng bộ. Cứ đổi shape một endpoint là đi sửa tay ở chỗ gọi, và TypeScript không bắt giúp.
Hono giải quyết cả hai cùng lúc, nên tôi chuyển sang nó cho lớp API.
Code thật: routing và middleware
Đây là phần làm tôi thấy "nhẹ đầu" ngay. Middleware không phải đi cài lẻ tẻ, nó nằm trong core:
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('*', logger())
app.use('/api/*', cors())
app.get('/api/health', (c) => c.json({ ok: true }))
app.get('/api/posts/:id', (c) => {
const id = c.req.param('id') // type là string, đã được suy sẵn
return c.json({ id })
})
export default app // Workers chỉ cần export default là chạy
Không có app.listen(), không có server.js. Trên Cloudflare Workers, export default app là đủ — vì bản thân Hono nói đúng giao thức fetch handler mà runtime mong đợi. Thứ tự middleware chạy đúng theo thứ tự đăng ký: phần "trước next()" của middleware đăng ký đầu chạy trước, phần "sau next()" chạy sau cùng. Hiểu rõ điều này quan trọng khi bạn xếp auth, cache, error handler.
Điểm "đắt" nhất: RPC type-safe không cần codegen
Đây là tính năng kéo tôi ở lại. Bạn định nghĩa route kèm validator, rồi export type cái route đó:
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const route = app.post(
'/posts',
zValidator('json', z.object({ title: z.string(), body: z.string() })),
(c) => {
const data = c.req.valid('json') // data đã được Zod xác thực + suy type
return c.json({ ok: true, id: 1 }, 201)
}
)
export type AppType = typeof route
Phía client, bạn tạo hc (Hono Client) và truyền AppType vào:
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('https://api.example.com/')
const res = await client.posts.$post({
json: { title: 'Hono', body: 'chạy thử trên Workers' },
})
if (res.ok) {
const data = await res.json() // data.id được suy type, không cần ép tay
}
Không có bước codegen, không có file OpenAPI sinh ra rồi phải build lại, không có server phụ. Type đi thẳng từ định nghĩa route sang chỗ gọi qua import thường. Nếu bạn đổi schema ở server, TypeScript đỏ ngay ở client. Đây chính là thứ tRPC làm được, nhưng Hono làm trên đúng nền HTTP/fetch chuẩn thay vì một giao thức riêng.
Bảng so sánh thực chiến: Hono vs Express vs Elysia
Số liệu dưới đây gom từ benchmark công khai và trải nghiệm trực tiếp — con số tuyệt đối tùy môi trường, nhưng thứ hạng tương đối thì ổn định.
| Tiêu chí | Express | Hono | Elysia |
|---|---|---|---|
| Bundle (minified) | ~572 KB | ~14–15 KB | ~15 KB |
| Runtime chạy được | Chủ yếu Node | Workers, Deno, Bun, Node, Lambda, Vercel | Bun-native |
| Middleware cốt lõi | Cài lẻ ≥5 package | Có sẵn trong core | Plugin system |
| Type-safety client | Gần như không | RPC hc() rất tốt |
Eden Treaty, tốt nhất |
| Throughput (trên Bun) | Thấp nhất | Cao | Cao nhất |
| Cold start trên edge | Nặng | Một con số mili-giây | Không phải trọng tâm |
Đọc bảng này như một architect: Elysia thắng về throughput thuần và độ "mượt" của type, nhưng nó cột chặt vào Bun. Hono đứng ở chỗ cân bằng nhất — nhanh, nhẹ, type tốt, và chạy ở mọi nơi. Express vẫn ổn cho monolith Node truyền thống, nhưng mang lên edge thì sai công cụ.
Gotcha thực chiến: những thứ ít ai nói trước
Không có framework nào miễn phí. Đây là ba thứ làm tôi mất thời gian:
1. Type của Hono "lạc quan" (optimistic). Biến đặt qua c.set() được khai báo type ở phạm vi toàn cục của app, nên TypeScript cho bạn đọc c.var.db kể cả ở route mà middleware set biến đó chưa từng chạy. Nó xanh trên IDE nhưng undefined lúc runtime.
app.use('/admin/*', async (c, next) => {
c.set('db', createDb(c.env))
await next()
})
app.get('/public', (c) => {
const db = c.var.db // TS không báo lỗi, nhưng route này không qua middleware trên -> undefined
})
Đây là đánh đổi có chủ đích để DX gọn, nhưng nó đòi kỷ luật: middleware nào set biến gì, route nào đi qua nó — phải tự mình giữ rõ trong đầu hoặc trong cấu trúc thư mục.
2. ESM-only. Hono và @hono/node-server chỉ chạy ESM. Nếu project còn dính CommonJS, bạn sẽ gặp lỗi resolve module lúc runtime. Đáng để biết trước khi nhét vào codebase cũ.
3. HonoX (meta-framework) còn alpha. Nếu bạn mơ một full-stack framework kiểu Next trên nền Hono, HonoX là hướng đó — nhưng minor version vẫn có thể breaking. Cho production hôm nay, công thức an toàn là Hono cho API + một frontend tách riêng, rồi theo dõi HonoX khi nó ổn định.
Một mẹo nhỏ từ docs mà tôi xác nhận đúng: với app nhiều route, compile trước rồi mới mở IDE, không thì TypeScript suy type cho RPC có thể làm editor ì. Và tránh c.notFound() khi dùng RPC — hãy c.json({...}, 404) để status code vào được type.
Khi nào nên và khi nào không nên dùng Hono?
Tóm tắt nhanh: Hono đáng dùng cho lớp API/microservice chạy ở edge và khi bạn muốn type-safe đầu-cuối mà không gánh tRPC/OpenAPI. Ngược lại, một monolith Express đang chạy ổn thì migrate chỉ để theo trend là âm giá trị.
Nhưng đây là một quyết định về chi phí và rủi ro nhiều hơn là về kỹ thuật — đặc biệt vì bạn không thể migrate từ Express sang Hono kiểu từng phần. Tôi tách riêng phần này thành một bài ra quyết định: Có nên dùng Hono cho production? Ma trận quyết định và chi phí migrate.
Câu hỏi thường gặp
Hono có thay thế được Express trong mọi dự án không?
Không, và cũng không nên nghĩ theo hướng đó. Hono mạnh nhất ở lớp API chạy edge và microservice — nơi bundle nhỏ và cold start là vấn đề thật. Một monolith Node đang chạy ổn với hệ sinh thái middleware Express thì không có lý do kỹ thuật để đập đi xây lại. Chọn công cụ theo bài toán và môi trường runtime, không theo benchmark đẹp.
RPC của Hono có giống tRPC không?
Về trải nghiệm thì giống: cả hai cho type chạy từ server sang client không cần codegen. Khác biệt là tRPC dựng một giao thức riêng phía trên HTTP, còn Hono giữ nguyên HTTP/fetch chuẩn — route vẫn là REST bình thường, client hc() chỉ là một lớp type mỏng phủ lên fetch. Nếu bạn muốn endpoint vẫn gọi được bằng curl như API thường mà vẫn type-safe trong TypeScript, mô hình của Hono hợp hơn.
Bài này nằm trong cụm về kiến trúc chạy ở biên: đọc thêm Edge AI & Computer Vision trong vận hành cho góc phần cứng/AI, hoặc Big Data chuỗi cung ứng 100+ triệu records cho góc kiến trúc dữ liệu lớn. Nếu doanh nghiệp của bạn đang cân nhắc một lớp API edge hay tái cấu trúc hạ tầng, xem thêm các câu chuyện dự án hoặc kết nối để trao đổi.