spinny:~/writing $ vim hono-framework-guide.md
1~2Trong 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.3~4[Hono](https://hono.dev) (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.5~6## Tại sao Hono7~81. **Hiệu năng.** `RegExpRouter` biê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.92. **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ào `Bun.serve`, mount trong server Deno hoặc adapt với `@hono/node-server`.103. **DX TypeScript-first.** Path parameter được suy luận thành literal type, RPC client an toàn kiểu end-to-end.11~12```mermaid13graph LR14 Client[Client] -->|Request| App[app.fetch]15 App --> MW1[Middleware 1]16 MW1 --> MW2[Middleware 2]17 MW2 --> Router[RegExpRouter]18 Router --> Handler[Route Handler]19 Handler --> Context[c.json / c.text]20 Context -->|Response| Client21 App -.->|deploy| CF[Cloudflare Workers]22 App -.->|deploy| Bun[Bun]23 App -.->|deploy| Deno[Deno]24 App -.->|deploy| Node[Node.js]25 App -.->|deploy| Vercel[Vercel]26```27~28## Bắt đầu29~30```bash31npm create hono@latest my-api32cd my-api33npm install34npm run dev35```36~37Starter 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:38~39```typescript40// src/index.ts41import { Hono } from 'hono'42~43const app = new Hono()44~45app.get('/', (c) => c.text('Hello Hono!'))46~47export default app48```49~50Trê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`.51~52## Routing53~54```typescript55import { Hono } from 'hono'56~57const app = new Hono()58~59app.get('/', (c) => c.text('Home'))60app.get('/posts/:id', (c) => {61 const id = c.req.param('id')62 return c.json({ id })63})64app.get('/posts/:id/comments/:commentId', (c) => {65 const { id, commentId } = c.req.param()66 return c.json({ id, commentId })67})68app.get('/files/*', (c) => c.text('Wildcard'))69app.post('/posts', async (c) => {70 const body = await c.req.json()71 return c.json({ created: body }, 201)72})73```74~75### Nhóm route76~77```typescript78// routes/posts.ts79import { Hono } from 'hono'80~81const posts = new Hono()82posts.get('/', (c) => c.json({ posts: [] }))83posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))84export default posts85~86// src/index.ts87import { Hono } from 'hono'88import posts from './routes/posts'89~90const app = new Hono()91app.route('/posts', posts)92```93~94## Đối tượng Context95~96```typescript97app.post('/echo', async (c) => {98 const userAgent = c.req.header('User-Agent')99 const page = c.req.query('page')100 const body = await c.req.json()101 const env = c.env102~103 c.set('requestId', crypto.randomUUID())104 const id = c.get('requestId')105~106 c.header('X-Request-Id', id)107 c.status(200)108 return c.json({ userAgent, page, body, id })109})110```111~112## Middleware: mô hình vỏ hành113~114```typescript115import { Hono } from 'hono'116import { logger } from 'hono/logger'117import { cors } from 'hono/cors'118import { secureHeaders } from 'hono/secure-headers'119~120const app = new Hono()121~122app.use('*', logger())123app.use('*', secureHeaders())124app.use('/api/*', cors({ origin: 'https://spinny.dev' }))125~126app.use('*', async (c, next) => {127 const start = performance.now()128 await next()129 c.header('X-Response-Time', `${performance.now() - start}ms`)130})131```132~133### Middleware tích hợp134~135| Middleware | Mục đích |136|-----------|---------|137| `logger` | Log có cấu trúc cho method, path, status, duration |138| `cors` | CORS cấu hình theo origin, method, header |139| `csrf` | Bảo vệ CSRF dựa trên origin |140| `secureHeaders` | Đặt CSP, HSTS, X-Frame-Options |141| `bearerAuth` / `basicAuth` | Auth Bearer/Basic sẵn dùng |142| `jwt` | Verify/sign JWT với `jose` |143| `etag` | Tạo ETag và xử lý 304 |144| `cache` | Cache qua Web Cache API |145| `compress` | gzip/deflate cho response |146| `bodyLimit` | Từ chối body vượt ngưỡng |147| `timing` | Header Server-Timing để profiling |148~149### Middleware tùy chỉnh type-safe150~151```typescript152import { createMiddleware } from 'hono/factory'153~154type AuthVars = { userId: string; role: 'user' | 'admin' }155~156export const requireAuth = createMiddleware<{ Variables: AuthVars }>(157 async (c, next) => {158 const token = c.req.header('Authorization')?.replace('Bearer ', '')159 if (!token) return c.json({ error: 'Unauthorized' }, 401)160~161 const payload = await verifyJwt(token)162 c.set('userId', payload.sub)163 c.set('role', payload.role)164 await next()165 }166)167~168app.get('/me', requireAuth, (c) => {169 const userId = c.var.userId170 return c.json({ userId })171})172```173~174## Validation với Zod175~176```typescript177import { Hono } from 'hono'178import { zValidator } from '@hono/zod-validator'179import { z } from 'zod'180~181const createPost = z.object({182 title: z.string().min(1).max(200),183 body: z.string().min(1),184 tags: z.array(z.string()).default([]),185})186~187app.post(188 '/posts',189 zValidator('json', createPost),190 (c) => {191 const data = c.req.valid('json')192 return c.json({ ok: true, post: data }, 201)193 }194)195```196~197## RPC: client type-safe end-to-end198~199```typescript200// server.ts201import { Hono } from 'hono'202import { zValidator } from '@hono/zod-validator'203import { z } from 'zod'204~205const app = new Hono()206 .get('/posts/:id', (c) =>207 c.json({ id: c.req.param('id'), title: 'Hello' })208 )209 .post(210 '/posts',211 zValidator('json', z.object({ title: z.string(), body: z.string() })),212 (c) => c.json({ ok: true }, 201)213 )214~215export type AppType = typeof app216export default app217```218~219```typescript220// client.ts221import { hc } from 'hono/client'222import type { AppType } from './server'223~224const client = hc<AppType>('https://api.spinny.dev')225~226const res = await client.posts[':id'].$get({ param: { id: '42' } })227if (res.ok) {228 const data = await res.json()229 console.log(data.title)230}231~232const created = await client.posts.$post({233 json: { title: 'Xin chào', body: 'Hono là một ngọn lửa' },234})235```236~237### Phân biệt theo status code238~239```typescript240.get('/posts/:id', (c) => {241 const post = findPost(c.req.param('id'))242 if (!post) return c.json({ error: 'not found' }, 404)243 return c.json({ post }, 200)244})245```246~247```typescript248const res = await client.posts[':id'].$get({ param: { id } })249if (res.status === 404) {250 const { error } = await res.json()251}252if (res.status === 200) {253 const { post } = await res.json()254}255```256~257## Router và hiệu năng258~259| Router | Điểm mạnh | Khi nào dùng |260|--------|-----------|-------------|261| `RegExpRouter` | Tốc độ cao nhất, regex biên dịch | Mặc định cho hầu hết API |262| `TrieRouter` | Hỗ trợ mọi pattern | Pattern phức tạp RegExp không xử lý được |263| `SmartRouter` | Tự động chọn tốt nhất | Mặc định khuyến nghị |264| `LinearRouter` | Đăng ký cực nhanh | Worker one-shot, cold start quan trọng |265| `PatternRouter` | Bundle tối thiểu (<15KB) | Hạn chế kích thước cực đoan |266~267```typescript268import { Hono } from 'hono'269import { LinearRouter } from 'hono/router/linear-router'270~271const app = new Hono({ router: new LinearRouter() })272```273~274## Deploy đa runtime275~276### Cloudflare Workers277~278```typescript279import { Hono } from 'hono'280~281type Bindings = { MY_KV: KVNamespace; DB: D1Database }282const app = new Hono<{ Bindings: Bindings }>()283~284app.get('/cache/:key', async (c) => {285 const value = await c.env.MY_KV.get(c.req.param('key'))286 return c.json({ value })287})288~289export default app290```291~292Deploy: `npx wrangler deploy`.293~294### Bun295~296```typescript297import { Hono } from 'hono'298const app = new Hono()299app.get('/', (c) => c.text('Bun + Hono'))300~301Bun.serve({ fetch: app.fetch, port: 3000 })302```303~304### Node.js305~306```typescript307import { serve } from '@hono/node-server'308import { Hono } from 'hono'309~310const app = new Hono()311app.get('/', (c) => c.text('Node + Hono'))312~313serve({ fetch: app.fetch, port: 3000 })314```315~316### Deno317~318```typescript319import { Hono } from 'jsr:@hono/hono'320const app = new Hono()321app.get('/', (c) => c.text('Deno + Hono'))322Deno.serve(app.fetch)323```324~325### Vercel326~327```typescript328// api/[[...route]].ts329import { Hono } from 'hono'330import { handle } from 'hono/vercel'331~332const app = new Hono().basePath('/api')333app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))334~335export const GET = handle(app)336export const POST = handle(app)337```338~339## Ví dụ thực tế: REST API với auth và DB340~341```typescript342import { Hono } from 'hono'343import { jwt } from 'hono/jwt'344import { logger } from 'hono/logger'345import { cors } from 'hono/cors'346import { zValidator } from '@hono/zod-validator'347import { z } from 'zod'348~349type Bindings = { DB: D1Database; JWT_SECRET: string }350type Variables = { jwtPayload: { sub: string } }351~352const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()353~354app.use('*', logger())355app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))356~357const auth = (c: any, next: any) =>358 jwt({ secret: c.env.JWT_SECRET })(c, next)359~360const api = app.basePath('/api')361~362api.get('/posts', async (c) => {363 const { results } = await c.env.DB364 .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50')365 .all()366 return c.json({ posts: results })367})368~369api.post(370 '/posts',371 auth,372 zValidator('json', z.object({373 title: z.string().min(1).max(200),374 body: z.string().min(1),375 })),376 async (c) => {377 const { title, body } = c.req.valid('json')378 const userId = c.var.jwtPayload.sub379 const result = await c.env.DB380 .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id')381 .bind(title, body, userId)382 .first<{ id: number }>()383 return c.json({ id: result?.id }, 201)384 }385)386~387api.onError((err, c) => {388 console.error(err)389 return c.json({ error: 'Internal error' }, 500)390})391~392export type AppType = typeof api393export default app394```395~396## Testing397~398```typescript399import { describe, it, expect } from 'vitest'400import app from '../src/index'401~402describe('GET /api/posts', () => {403 it('trả về danh sách post', async () => {404 const res = await app.request('/api/posts')405 expect(res.status).toBe(200)406 const body = await res.json()407 expect(body.posts).toBeInstanceOf(Array)408 })409})410```411~412## Best practice413~414### 1. Chain các định nghĩa route415~416```typescript417const app = new Hono()418 .get('/posts', handler1)419 .post('/posts', handler2)420 .get('/posts/:id', handler3)421```422~423### 2. Export type, không phải implementation424~425Client phải import `AppType`.426~427### 3. Một router cho mỗi domain428~429Sub-app cho `posts`, `users`, `webhooks`.430~431### 4. Validation tại biên, luôn luôn432~433Mọi input bên ngoài phải qua `zValidator`.434~435### 5. Dựa vào binding, không phải client toàn cục436~437Trên Cloudflare, truy cập KV/D1/R2 qua `c.env`.438~439### 6. Đo lường trước khi tối ưu router440~441`SmartRouter` mặc định phù hợp 95% trường hợp.442~443## Kết luận444~445Hono đã 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.446~447> **Checklist bắt đầu:**448>449> - [x] `npm create hono@latest` và chọn template runtime của bạn450> - [x] Định nghĩa route bằng chaining (`.get(...).post(...)`)451> - [x] Thêm `logger`, `cors`, `secureHeaders` làm middleware toàn cục452> - [x] Validate mọi input với `@hono/zod-validator`453> - [x] Export `AppType` và tiêu thụ API với client `hc` type-safe454> - [x] Viết test với `app.request()` — không cần server HTTP455> - [x] Deploy với `wrangler deploy` (CF), `vercel deploy` hoặc bundler của runtime bạn456~
NORMAL · hono-framework-guide.md [readonly]456 lines · :q to close