Trong nhiều năm, chọn một framework web JavaScript có nghĩa là chấp nhận đánh đổi: Express phổ quát nhưng chậm và bị ràng buộc với Node, Fastify nhanh nhưng chỉ Node, Next.js đầy đủ nhưng nặng. Khi các runtime edge — Cloudflare Workers, Deno Deploy, Bun, Vercel Edge — xuất hiện, các framework đó đã bộc lộ giới hạn: dependency không tương thích, bundle khổng lồ, API gắn với req/res của Node.
Hono (tiếng Nhật nghĩa là "ngọn lửa" 🔥) là câu trả lời hiện đại. Một framework dưới 14KB, được xây dựng hoàn toàn trên Web Standards (Request, Response, fetch), chạy ở bất cứ đâu có runtime JavaScript. Cùng một mã nguồn deploy được lên Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify và AWS Lambda — không cần sửa đổi.
Tại sao Hono
- Hiệu năng.
RegExpRouterbiên dịch tất cả mẫu route thành một regex duy nhất, tránh các vòng lặp tuyến tính của router truyền thống. Benchmark vượt 400.000 ops/giây. - Tính di động. Web Standards có nghĩa là không phụ thuộc Node. Cùng một
app.fetchđược export làm default trong Cloudflare Worker, được truyền vàoBun.serve, mount trong server Deno hoặc adapt với@hono/node-server. - DX TypeScript-first. Path parameter được suy luận thành literal type, RPC client an toàn kiểu end-to-end.
Bắt đầu
npm create hono@latest my-api cd my-api npm install npm run dev
Starter hỏi dùng template nào: cloudflare-workers, bun, deno, nodejs, vercel, aws-lambda, nextjs và nhiều hơn. Để thử nhanh, có thể bắt đầu từ một file:
// src/index.ts import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hello Hono!')) export default app
Trên Cloudflare Workers thế là đủ. Trên Bun: Bun.serve({ fetch: app.fetch, port: 3000 }). Trên Node: serve({ fetch: app.fetch }) từ @hono/node-server.
Routing
import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Home')) app.get('/posts/:id', (c) => { const id = c.req.param('id') return c.json({ id }) }) app.get('/posts/:id/comments/:commentId', (c) => { const { id, commentId } = c.req.param() return c.json({ id, commentId }) }) app.get('/files/*', (c) => c.text('Wildcard')) app.post('/posts', async (c) => { const body = await c.req.json() return c.json({ created: body }, 201) })
Nhóm route
// routes/posts.ts import { Hono } from 'hono' const posts = new Hono() posts.get('/', (c) => c.json({ posts: [] })) posts.get('/:id', (c) => c.json({ id: c.req.param('id') })) export default posts // src/index.ts import { Hono } from 'hono' import posts from './routes/posts' const app = new Hono() app.route('/posts', posts)
Đối tượng Context
app.post('/echo', async (c) => { const userAgent = c.req.header('User-Agent') const page = c.req.query('page') const body = await c.req.json() const env = c.env c.set('requestId', crypto.randomUUID()) const id = c.get('requestId') c.header('X-Request-Id', id) c.status(200) return c.json({ userAgent, page, body, id }) })
Middleware: mô hình vỏ hành
import { Hono } from 'hono' import { logger } from 'hono/logger' import { cors } from 'hono/cors' import { secureHeaders } from 'hono/secure-headers' const app = new Hono() app.use('*', logger()) app.use('*', secureHeaders()) app.use('/api/*', cors({ origin: 'https://spinny.dev' })) app.use('*', async (c, next) => { const start = performance.now() await next() c.header('X-Response-Time', `${performance.now() - start}ms`) })
Middleware tích hợp
| Middleware | Mục đích |
|---|---|
logger | Log có cấu trúc cho method, path, status, duration |
cors | CORS cấu hình theo origin, method, header |
csrf | Bảo vệ CSRF dựa trên origin |
secureHeaders | Đặt CSP, HSTS, X-Frame-Options |
bearerAuth / basicAuth | Auth Bearer/Basic sẵn dùng |
jwt | Verify/sign JWT với jose |
etag | Tạo ETag và xử lý 304 |
cache | Cache qua Web Cache API |
compress | gzip/deflate cho response |
bodyLimit | Từ chối body vượt ngưỡng |
timing | Header Server-Timing để profiling |
Middleware tùy chỉnh type-safe
import { createMiddleware } from 'hono/factory' type AuthVars = { userId: string; role: 'user' | 'admin' } export const requireAuth = createMiddleware<{ Variables: AuthVars }>( async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', '') if (!token) return c.json({ error: 'Unauthorized' }, 401) const payload = await verifyJwt(token) c.set('userId', payload.sub) c.set('role', payload.role) await next() } ) app.get('/me', requireAuth, (c) => { const userId = c.var.userId return c.json({ userId }) })
Validation với Zod
import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const createPost = z.object({ title: z.string().min(1).max(200), body: z.string().min(1), tags: z.array(z.string()).default([]), }) app.post( '/posts', zValidator('json', createPost), (c) => { const data = c.req.valid('json') return c.json({ ok: true, post: data }, 201) } )
RPC: client type-safe end-to-end
// server.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const app = new Hono() .get('/posts/:id', (c) => c.json({ id: c.req.param('id'), title: 'Hello' }) ) .post( '/posts', zValidator('json', z.object({ title: z.string(), body: z.string() })), (c) => c.json({ ok: true }, 201) ) export type AppType = typeof app export default app
// client.ts import { hc } from 'hono/client' import type { AppType } from './server' const client = hc<AppType>('https://api.spinny.dev') const res = await client.posts[':id'].$get({ param: { id: '42' } }) if (res.ok) { const data = await res.json() console.log(data.title) } const created = await client.posts.$post({ json: { title: 'Xin chào', body: 'Hono là một ngọn lửa' }, })
Phân biệt theo status code
.get('/posts/:id', (c) => { const post = findPost(c.req.param('id')) if (!post) return c.json({ error: 'not found' }, 404) return c.json({ post }, 200) })
const res = await client.posts[':id'].$get({ param: { id } }) if (res.status === 404) { const { error } = await res.json() } if (res.status === 200) { const { post } = await res.json() }
Router và hiệu năng
| Router | Điểm mạnh | Khi nào dùng |
|---|---|---|
RegExpRouter | Tốc độ cao nhất, regex biên dịch | Mặc định cho hầu hết API |
TrieRouter | Hỗ trợ mọi pattern | Pattern phức tạp RegExp không xử lý được |
SmartRouter | Tự động chọn tốt nhất | Mặc định khuyến nghị |
LinearRouter | Đăng ký cực nhanh | Worker one-shot, cold start quan trọng |
PatternRouter | Bundle tối thiểu (<15KB) | Hạn chế kích thước cực đoan |
import { Hono } from 'hono' import { LinearRouter } from 'hono/router/linear-router' const app = new Hono({ router: new LinearRouter() })
Deploy đa runtime
Cloudflare Workers
import { Hono } from 'hono' type Bindings = { MY_KV: KVNamespace; DB: D1Database } const app = new Hono<{ Bindings: Bindings }>() app.get('/cache/:key', async (c) => { const value = await c.env.MY_KV.get(c.req.param('key')) return c.json({ value }) }) export default app
Deploy: npx wrangler deploy.
Bun
import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Bun + Hono')) Bun.serve({ fetch: app.fetch, port: 3000 })
Node.js
import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Node + Hono')) serve({ fetch: app.fetch, port: 3000 })
Deno
import { Hono } from 'jsr:@hono/hono' const app = new Hono() app.get('/', (c) => c.text('Deno + Hono')) Deno.serve(app.fetch)
Vercel
// api/[[...route]].ts import { Hono } from 'hono' import { handle } from 'hono/vercel' const app = new Hono().basePath('/api') app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' })) export const GET = handle(app) export const POST = handle(app)
Ví dụ thực tế: REST API với auth và DB
import { Hono } from 'hono' import { jwt } from 'hono/jwt' import { logger } from 'hono/logger' import { cors } from 'hono/cors' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' type Bindings = { DB: D1Database; JWT_SECRET: string } type Variables = { jwtPayload: { sub: string } } const app = new Hono<{ Bindings: Bindings; Variables: Variables }>() app.use('*', logger()) app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true })) const auth = (c: any, next: any) => jwt({ secret: c.env.JWT_SECRET })(c, next) const api = app.basePath('/api') api.get('/posts', async (c) => { const { results } = await c.env.DB .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50') .all() return c.json({ posts: results }) }) api.post( '/posts', auth, zValidator('json', z.object({ title: z.string().min(1).max(200), body: z.string().min(1), })), async (c) => { const { title, body } = c.req.valid('json') const userId = c.var.jwtPayload.sub const result = await c.env.DB .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id') .bind(title, body, userId) .first<{ id: number }>() return c.json({ id: result?.id }, 201) } ) api.onError((err, c) => { console.error(err) return c.json({ error: 'Internal error' }, 500) }) export type AppType = typeof api export default app
Testing
import { describe, it, expect } from 'vitest' import app from '../src/index' describe('GET /api/posts', () => { it('trả về danh sách post', async () => { const res = await app.request('/api/posts') expect(res.status).toBe(200) const body = await res.json() expect(body.posts).toBeInstanceOf(Array) }) })
Best practice
1. Chain các định nghĩa route
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
2. Export type, không phải implementation
Client phải import AppType.
3. Một router cho mỗi domain
Sub-app cho posts, users, webhooks.
4. Validation tại biên, luôn luôn
Mọi input bên ngoài phải qua zValidator.
5. Dựa vào binding, không phải client toàn cục
Trên Cloudflare, truy cập KV/D1/R2 qua c.env.
6. Đo lường trước khi tối ưu router
SmartRouter mặc định phù hợp 95% trường hợp.
Kết luận
Hono đã trở thành tiêu chuẩn de facto vào năm 2026 để xây dựng API sẵn sàng cho edge bằng TypeScript.
Checklist bắt đầu:
npm create hono@latestvà chọn template runtime của bạn- Định nghĩa route bằng chaining (
.get(...).post(...))- Thêm
logger,cors,secureHeaderslàm middleware toàn cục- Validate mọi input với
@hono/zod-validator- Export
AppTypevà tiêu thụ API với clienthctype-safe- Viết test với
app.request()— không cần server HTTP- Deploy với
wrangler deploy(CF),vercel deployhoặc bundler của runtime bạn