Hono framework: Trải nghiệm thực chiến trên Web Standards

Đưa Hono lên Cloudflare Workers chạy thật: routing, middleware, RPC type-safe không codegen, benchmark và những gotcha dưới góc nhìn System Architect.

Hono framework: Trải nghiệm thực chiến trên Web Standards

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 install năm package. Chế độ RPC chia sẻ type từ server sang client chỉ bằng một dòng export 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:

  1. 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ì.
  2. 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.

Tôi mổ xẻ riêng cách dựng lớp RPC này an toàn từ đầu tới cuối — validator Zod, xử lý lỗi ở biên, và các bẫy type "lạc quan" cần né — trong bài Hono RPC + Zod: type-safe end-to-end.

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 lại câu hỏi: adopt framework là quyết định Delivery, không phải kỹ thuật

Một benchmark đẹp thuyết phục được kỹ sư trong 5 phút. Nhưng thứ quyết định một adoption thành công hay thất bại trên thực tế là: chi phí viết lại, thời gian đội ngũ làm quen, độ sẵn của thư viện khi gặp ca lạ, và việc tuyển được người duy trì nó sau 2 năm. Đó đều là biến số Delivery, không phải biến số trong bài đo tốc độ.

Nói cách khác: Hono "nhanh hơn Express" gần như không liên quan đến việc bạn có nên đưa nó vào hệ thống đang chạy hay không. Dưới đây là khung tôi dùng.

Vì sao chi phí migrate là biến số lớn nhất

Đây là điểm nhiều người bỏ qua: bạn không thể chuyển dần từng route từ Express sang Hono trong cùng một app. Express bám vào req/res của Node; Hono bám vào Request/Response của Web Standards. Hai mô hình request/response khác nhau về bản chất, nên không có đường "bọc lại và chạy song song" sạch sẽ trong một codebase.

Hệ quả thực tế:

  • Mọi route handler phải viết lại, không phải sửa.
  • Middleware Express đặc thù (passport, multer, express-session...) phải thay bằng bản tương thích Web Standards — phần lớn có sẵn trong hệ @hono/*, nhưng không phải 1-1 cho mọi case.
  • Vì vậy chiến lược an toàn là strangler pattern ở mức service: dựng service/microservice mới bằng Hono, để cái cũ chạy tiếp, không trộn hai framework trong một app.

Nghĩa là chi phí migrate không tuyến tính theo "số route" — nó là một quyết định viết-lại-từng-mảng. Với một codebase Express lớn đang ổn định, con số này thường lớn hơn lợi ích, và câu trả lời là không.

Ma trận quyết định 5 trục

Tôi chấm Hono theo 5 trục. Nếu phần lớn nghiêng về cột phải, hãy adopt; nếu nghiêng trái, đừng.

Trục Nghiêng "KHÔNG dùng Hono" Nghiêng "NÊN dùng Hono"
Runtime đích Node truyền thống, server long-running Edge (Workers/Deno/Vercel), đa runtime
Nhu cầu type-safety API nội bộ nhỏ, ít client Cần type chạy thẳng server→client, không muốn codegen
Trạng thái hiện tại Express monolith lớn đang chạy ổn Greenfield hoặc tách service mới
Đội ngũ / hiring Team quen Express, tuyển theo Express Team TS-first, build trên edge/Bun
Chấp nhận rủi ro vùng rìa Cần mọi middleware có sẵn, zero tự viết OK tự viết middleware khi ecosystem chưa có

Quy tắc đọc: trục Trạng thái hiện tại và Runtime đích có trọng số nặng nhất, vì chúng quyết định chi phí migrate. Hai trục này một mình đủ để loại hoặc chọn trong đa số trường hợp.

Chi phí ẩn ít ai tính trước

Ngoài migrate, có vài khoản chi phí không nằm trên benchmark:

  • Ecosystem mới ~4 năm tuổi. Lõi Hono đã production-ready và download tăng rất mạnh (lên hàng chục triệu/tuần năm 2026), nhưng vẫn trẻ hơn Express một thập kỷ. Thỉnh thoảng bạn sẽ phải tự viết một middleware mà ở Express chỉ là một dòng npm install. Tính giờ kỹ sư cho khoản này.
  • Hiring và bus factor. Express có nguồn ứng viên dày và cả thập kỷ tài liệu. Hono đang chiếm phần lớn app backend Node mới, nhưng pool người đã chạy nó ở production vẫn nhỏ hơn. Hỏi: sau 2 năm, ai duy trì nó?
  • Observability ở edge. Chạy trên Workers nghĩa là bạn vào mô hình quan sát của edge: log/metric phải đi qua công cụ của nền tảng, rate limit phân tán thường cần Durable Objects, cache dùng Cache API. Không khó, nhưng là một mô hình vận hành khác — có chi phí học.
  • ESM-only. Nếu hạ tầng cũ còn CommonJS, đây là một khoản dọn dẹp phải tính.

Ba kịch bản ra quyết định

Kịch bản A — Greenfield edge API → NÊN. Bạn dựng mới một lớp API/gateway hoặc microservice chạy trên Cloudflare Workers/Deno, cần cold start mili-giây và type-safe đầu-cuối. Đây là sân nhà của Hono. Không có chi phí migrate, chỉ có lợi. Adopt.

Kịch bản B — Express monolith đang chạy ổn → KHÔNG. Bạn có một app Express lớn, nhiều middleware, đang phục vụ tốt. Migrate đồng nghĩa viết lại toàn bộ handler và thay middleware — chi phí lớn, rủi ro cao, lợi ích biên thấp. Nếu thật sự cần edge, hãy tách service mới bằng Hono theo strangler pattern, đừng đụng vào monolith.

Kịch bản C — Cần full-stack framework production ngay → CHỜ. Bạn muốn một thứ kiểu Next nhưng trên nền Hono (HonoX). Hướng đúng, nhưng HonoX còn alpha và minor version có thể breaking. Cho production hôm nay: Hono cho API + frontend tách riêng, theo dõi HonoX đến khi ổn định.


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.

Câu hỏi thường gặp

Hono framework là gì và khác Express ở đâu?

Hono là web framework nhẹ xây trên Web Standards — dùng đúng object Request và Response như fetch() trên trình duyệt, nên chạy được trên Cloudflare Workers, Bun, Deno, Node, Lambda mà không đổi code. Express bám vào API riêng của Node nên khó mang ra môi trường edge.

Khi nào KHÔNG nên dùng Hono?

Khi team đang ổn định trên một hệ sinh thái Node thuần với nhiều middleware Express cũ, hoặc cần một full-stack meta-framework production ổn định ngay (HonoX vẫn còn alpha). Hono mạnh nhất ở lớp API chạy edge và microservice, không phải để thay mọi monolith Node đang chạy tốt.

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.

Có thể migrate dần từ Express sang Hono không?

Không theo kiểu từng route một trong cùng một app. Express dựa trên req/res của Node còn Hono dựa trên Request/Response của Web Standards — hai mô hình khác nhau nên handler phải viết lại và middleware Express phải thay bằng bản tương thích. Cách an toàn là chạy Hono cho service mới hoặc tách microservice, không cố trộn hai framework trong một codebase.

Hono đã đủ trưởng thành cho production chưa?

Lõi Hono đã production-ready và được dùng rộng ở các API chạy edge, với middleware JWT/CORS/cache/logger sẵn trong core. Điểm cần cân nhắc là ecosystem mới ~4 năm tuổi nên đôi khi phải tự viết middleware mà Express đã có sẵn, và meta-framework HonoX vẫn còn alpha. Lõi: ổn. Vùng rìa: tự kiểm.

Có thể chạy Hono và Express song song trong lúc chuyển đổi không?

Có, nhưng ở mức service riêng, không phải trộn trong một app. Cách thực tế là dựng service mới (hoặc một nhóm endpoint mới) bằng Hono, đặt sau một reverse proxy/gateway, rồi rút dần lưu lượng khỏi app Express cũ — strangler pattern. Trộn hai framework trong cùng một process là tự rước nợ vì hai mô hình request/response không tương thích trực tiếp.

Hono có phải lựa chọn an toàn về lâu dài không?

Với lớp edge API thì có cơ sở để tin: nó xây trên Web Standards nên ít phụ thuộc một runtime, download tăng mạnh và đang là mặc định cho nhiều app backend mới. Rủi ro dài hạn không nằm ở lõi mà ở vùng rìa — ecosystem trẻ và HonoX chưa ổn. Nếu bạn dùng nó đúng vai trò (API edge, microservice) thay vì ép làm mọi thứ, đó là một lựa chọn hợp lý về lâu dài.