spinny:~/writing $ vim hono-framework-guide.md
1~2หลายปีที่ผ่านมา การเลือกเฟรมเวิร์กเว็บ JavaScript หมายถึงการยอมรับการแลกเปลี่ยน Express ใช้ได้สากลแต่ช้าและผูกกับ Node, Fastify เร็วแต่เฉพาะ Node, Next.js ครบครันแต่หนัก เมื่อ runtime แบบ edge — Cloudflare Workers, Deno Deploy, Bun, Vercel Edge — มาถึง เฟรมเวิร์กเหล่านั้นเผยข้อจำกัด3~4[Hono](https://hono.dev) (ภาษาญี่ปุ่นแปลว่า "เปลวไฟ" 🔥) คือคำตอบสมัยใหม่ เฟรมเวิร์กขนาดต่ำกว่า 14KB ที่สร้างบนมาตรฐานเว็บทั้งหมด (`Request`, `Response`, `fetch`) ทำงานทุกที่ที่มี runtime JavaScript รหัสเดียวกัน deploy ได้บน Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify และ AWS Lambda โดยไม่มีการเปลี่ยนแปลง5~6## ทำไมต้อง Hono7~81. **ประสิทธิภาพ** `RegExpRouter` คอมไพล์รูปแบบเส้นทางทั้งหมดเป็น regex เดียว หลีกเลี่ยงลูปเชิงเส้นของเราเตอร์แบบดั้งเดิม Benchmark เกิน 400,000 ops/วินาที92. **ความสามารถในการพกพา** มาตรฐานเว็บหมายถึงไม่มี dependency กับ Node เลย `app.fetch` เดียวกัน export เป็น default ใน Cloudflare Worker ส่งให้ `Bun.serve` mount ใน Deno server หรือปรับด้วย `@hono/node-server`103. **DX แบบ TypeScript-first** Path parameter ถูก infer เป็น literal type, RPC client type-safe end-to-end11~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## เริ่มต้น29~30```bash31npm create hono@latest my-api32cd my-api33npm install34npm run dev35```36~37Starter ถามว่าใช้ template ไหน: `cloudflare-workers`, `bun`, `deno`, `nodejs`, `vercel`, `aws-lambda`, `nextjs` และอื่นๆ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~50บน Cloudflare Workers แค่นี้พอ บน Bun: `Bun.serve({ fetch: app.fetch, port: 3000 })` บน Node: `serve({ fetch: app.fetch })` จาก `@hono/node-server`51~52## การกำหนดเส้นทาง53~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### การจัดกลุ่มเส้นทาง76~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## ออบเจ็กต์ 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## มิดเดิลแวร์: โมเดลหัวหอม113~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### มิดเดิลแวร์ในตัว134~135| มิดเดิลแวร์ | หน้าที่ |136|-----------|---------|137| `logger` | ล็อกแบบมีโครงสร้างของ method, path, status, ระยะเวลา |138| `cors` | CORS ปรับได้ตาม origin, methods, headers |139| `csrf` | การป้องกัน CSRF ตาม origin |140| `secureHeaders` | ตั้ง CSP, HSTS, X-Frame-Options |141| `bearerAuth` / `basicAuth` | Bearer/Basic auth พร้อมใช้ |142| `jwt` | ตรวจสอบ/เซ็น JWT ด้วย `jose` |143| `etag` | สร้าง ETag และจัดการ 304 |144| `cache` | แคชผ่าน Web Cache API |145| `compress` | gzip/deflate สำหรับ response |146| `bodyLimit` | ปฏิเสธ body เกินเกณฑ์ |147| `timing` | ส่วนหัว Server-Timing สำหรับ profiling |148~149### มิดเดิลแวร์กำหนดเอง 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## การตรวจสอบด้วย 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: ไคลเอ็นต์ 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: 'สวัสดี', body: 'Hono คือเปลวไฟ' },234})235```236~237### การแยกตามรหัสสถานะ238~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## เราเตอร์และประสิทธิภาพ258~259| เราเตอร์ | จุดเด่น | ใช้เมื่อใด |260|--------|-----------|-------------|261| `RegExpRouter` | ความเร็วสูงสุด regex คอมไพล์แล้ว | ค่าเริ่มต้นสำหรับ API ส่วนใหญ่ |262| `TrieRouter` | รองรับทุก pattern | Pattern ซับซ้อนที่ RegExp จัดการไม่ได้ |263| `SmartRouter` | เลือกสิ่งที่ดีที่สุดอัตโนมัติ | ค่าเริ่มต้นที่แนะนำ |264| `LinearRouter` | การลงทะเบียนเร็วสุด | Worker one-shot, cold start สำคัญ |265| `PatternRouter` | Bundle ขั้นต่ำ (<15KB) | ข้อจำกัดขนาดสุดขีด |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 หลาย 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## ตัวอย่างใช้งานจริง: REST API พร้อม auth และ 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## การทดสอบ397~398```typescript399import { describe, it, expect } from 'vitest'400import app from '../src/index'401~402describe('GET /api/posts', () => {403 it('คืนค่ารายการโพสต์', 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## แนวปฏิบัติที่ดีที่สุด413~414### 1. ทำ chain การกำหนด 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 ไม่ใช่ implementation424~425ไคลเอ็นต์ต้อง import `AppType`426~427### 3. หนึ่ง router ต่อหนึ่ง domain428~429Sub-app สำหรับ `posts`, `users`, `webhooks`430~431### 4. ตรวจสอบที่ขอบ เสมอ432~433ทุก input ภายนอกต้องผ่าน `zValidator`434~435### 5. พึ่งพา binding ไม่ใช่ client ระดับ global436~437บน Cloudflare เข้าถึง KV/D1/R2 ผ่าน `c.env`438~439### 6. วัดก่อนปรับแต่ง router440~441`SmartRouter` เริ่มต้นเหมาะกับ 95% ของกรณี442~443## บทสรุป444~445Hono กลายเป็นมาตรฐาน de facto ในปี 2026 สำหรับการสร้าง API ที่พร้อมสำหรับ edge ด้วย TypeScript446~447> **เช็คลิสต์เริ่มต้น:**448>449> - [x] `npm create hono@latest` และเลือก template runtime ของคุณ450> - [x] กำหนดเส้นทางด้วย chaining (`.get(...).post(...)`)451> - [x] เพิ่ม `logger`, `cors`, `secureHeaders` เป็นมิดเดิลแวร์ระดับ global452> - [x] ตรวจสอบทุก input ด้วย `@hono/zod-validator`453> - [x] Export `AppType` และใช้ API ด้วยไคลเอ็นต์ `hc` type-safe454> - [x] เขียน test ด้วย `app.request()` — ไม่ต้องมี HTTP server455> - [x] Deploy ด้วย `wrangler deploy` (CF), `vercel deploy` หรือ bundler ของ runtime456~
NORMAL · hono-framework-guide.md [readonly]456 lines · :q to close