spinny:~/writing $ vim hono-framework-guide.md
1~2Selama bertahun-tahun, memilih rangka kerja web JavaScript bermakna menerima kompromi: Express adalah sejagat tetapi perlahan dan terikat dengan Node, Fastify pantas tetapi hanya Node, Next.js berciri penuh tetapi berat. Apabila runtime edge — Cloudflare Workers, Deno Deploy, Bun, Vercel Edge — muncul, rangka kerja itu menunjukkan hadnya.3~4[Hono](https://hono.dev) (Jepun untuk "api" 🔥) ialah jawapan moden. Rangka kerja di bawah 14KB, dibina sepenuhnya di atas Web Standards (`Request`, `Response`, `fetch`), berjalan di mana-mana terdapat runtime JavaScript. Kod yang sama disebarkan ke Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify dan AWS Lambda — tanpa perubahan.5~6## Mengapa Hono7~81. **Prestasi.** `RegExpRouter` mengkompil semua corak laluan kepada satu regex. Penanda aras melebihi 400,000 ops/s.92. **Mudah alih.** Web Standards bermaksud sifar kebergantungan Node.103. **DX TypeScript-first.** Parameter laluan disimpulkan sebagai literal type, klien RPC selamat-jenis 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## Memulakan29~30```bash31npm create hono@latest my-api32cd my-api33npm install34npm run dev35```36~37```typescript38// src/index.ts39import { Hono } from 'hono'40~41const app = new Hono()42~43app.get('/', (c) => c.text('Hello Hono!'))44~45export default app46```47~48Pada Cloudflare Workers, ini sudah memadai. Pada Bun: `Bun.serve({ fetch: app.fetch, port: 3000 })`. Pada Node: `serve({ fetch: app.fetch })` daripada `@hono/node-server`.49~50## Routing51~52```typescript53import { Hono } from 'hono'54~55const app = new Hono()56~57app.get('/', (c) => c.text('Home'))58app.get('/posts/:id', (c) => {59 const id = c.req.param('id')60 return c.json({ id })61})62app.get('/posts/:id/comments/:commentId', (c) => {63 const { id, commentId } = c.req.param()64 return c.json({ id, commentId })65})66app.get('/files/*', (c) => c.text('Wildcard'))67app.post('/posts', async (c) => {68 const body = await c.req.json()69 return c.json({ created: body }, 201)70})71```72~73### Pengelompokan laluan74~75```typescript76// routes/posts.ts77import { Hono } from 'hono'78~79const posts = new Hono()80posts.get('/', (c) => c.json({ posts: [] }))81posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))82export default posts83~84// src/index.ts85import { Hono } from 'hono'86import posts from './routes/posts'87~88const app = new Hono()89app.route('/posts', posts)90```91~92## Objek Context93~94```typescript95app.post('/echo', async (c) => {96 const userAgent = c.req.header('User-Agent')97 const page = c.req.query('page')98 const body = await c.req.json()99 const env = c.env100~101 c.set('requestId', crypto.randomUUID())102 const id = c.get('requestId')103~104 c.header('X-Request-Id', id)105 c.status(200)106 return c.json({ userAgent, page, body, id })107})108```109~110## Middleware: model bawang111~112```typescript113import { Hono } from 'hono'114import { logger } from 'hono/logger'115import { cors } from 'hono/cors'116import { secureHeaders } from 'hono/secure-headers'117~118const app = new Hono()119~120app.use('*', logger())121app.use('*', secureHeaders())122app.use('/api/*', cors({ origin: 'https://spinny.dev' }))123~124app.use('*', async (c, next) => {125 const start = performance.now()126 await next()127 c.header('X-Response-Time', `${performance.now() - start}ms`)128})129```130~131### Middleware terbina132~133| Middleware | Tujuan |134|-----------|---------|135| `logger` | Log berstruktur kaedah, laluan, status, tempoh |136| `cors` | CORS boleh dikonfigurasi mengikut origin, kaedah, header |137| `csrf` | Perlindungan CSRF berasaskan origin |138| `secureHeaders` | Menetapkan CSP, HSTS, X-Frame-Options |139| `bearerAuth` / `basicAuth` | Auth Bearer/Basic siap pakai |140| `jwt` | JWT sahkan/tandatangan dengan `jose` |141| `etag` | Menjana ETag dan mengendalikan 304 |142| `cache` | Cache melalui Web Cache API |143| `compress` | gzip/deflate pada respons |144| `bodyLimit` | Menolak body melebihi ambang |145| `timing` | Header Server-Timing untuk profiling |146~147### Middleware tersuai selamat-jenis148~149```typescript150import { createMiddleware } from 'hono/factory'151~152type AuthVars = { userId: string; role: 'user' | 'admin' }153~154export const requireAuth = createMiddleware<{ Variables: AuthVars }>(155 async (c, next) => {156 const token = c.req.header('Authorization')?.replace('Bearer ', '')157 if (!token) return c.json({ error: 'Unauthorized' }, 401)158~159 const payload = await verifyJwt(token)160 c.set('userId', payload.sub)161 c.set('role', payload.role)162 await next()163 }164)165~166app.get('/me', requireAuth, (c) => {167 const userId = c.var.userId168 return c.json({ userId })169})170```171~172## Validasi dengan Zod173~174```typescript175import { Hono } from 'hono'176import { zValidator } from '@hono/zod-validator'177import { z } from 'zod'178~179const createPost = z.object({180 title: z.string().min(1).max(200),181 body: z.string().min(1),182 tags: z.array(z.string()).default([]),183})184~185app.post(186 '/posts',187 zValidator('json', createPost),188 (c) => {189 const data = c.req.valid('json')190 return c.json({ ok: true, post: data }, 201)191 }192)193```194~195## RPC: klien selamat-jenis end-to-end196~197```typescript198// server.ts199import { Hono } from 'hono'200import { zValidator } from '@hono/zod-validator'201import { z } from 'zod'202~203const app = new Hono()204 .get('/posts/:id', (c) =>205 c.json({ id: c.req.param('id'), title: 'Hello' })206 )207 .post(208 '/posts',209 zValidator('json', z.object({ title: z.string(), body: z.string() })),210 (c) => c.json({ ok: true }, 201)211 )212~213export type AppType = typeof app214export default app215```216~217```typescript218// client.ts219import { hc } from 'hono/client'220import type { AppType } from './server'221~222const client = hc<AppType>('https://api.spinny.dev')223~224const res = await client.posts[':id'].$get({ param: { id: '42' } })225if (res.ok) {226 const data = await res.json()227 console.log(data.title)228}229~230const created = await client.posts.$post({231 json: { title: 'Helo', body: 'Hono adalah api' },232})233```234~235### Diskriminasi kod status236~237```typescript238.get('/posts/:id', (c) => {239 const post = findPost(c.req.param('id'))240 if (!post) return c.json({ error: 'not found' }, 404)241 return c.json({ post }, 200)242})243```244~245```typescript246const res = await client.posts[':id'].$get({ param: { id } })247if (res.status === 404) {248 const { error } = await res.json()249}250if (res.status === 200) {251 const { post } = await res.json()252}253```254~255## Penghala dan prestasi256~257| Penghala | Kekuatan | Bila digunakan |258|--------|-----------|-------------|259| `RegExpRouter` | Kelajuan maksimum, regex terkompil | Lalai untuk kebanyakan API |260| `TrieRouter` | Menyokong semua corak | Corak kompleks yang RegExp tidak boleh kendalikan |261| `SmartRouter` | Memilih yang terbaik secara automatik | Lalai disyorkan |262| `LinearRouter` | Pendaftaran ultra-laju | Pekerja sekali tembak, cold start kritikal |263| `PatternRouter` | Bundle minimum (<15KB) | Kekangan saiz melampau |264~265```typescript266import { Hono } from 'hono'267import { LinearRouter } from 'hono/router/linear-router'268~269const app = new Hono({ router: new LinearRouter() })270```271~272## Deploy pelbagai runtime273~274### Cloudflare Workers275~276```typescript277import { Hono } from 'hono'278~279type Bindings = { MY_KV: KVNamespace; DB: D1Database }280const app = new Hono<{ Bindings: Bindings }>()281~282app.get('/cache/:key', async (c) => {283 const value = await c.env.MY_KV.get(c.req.param('key'))284 return c.json({ value })285})286~287export default app288```289~290Deploy: `npx wrangler deploy`.291~292### Bun293~294```typescript295import { Hono } from 'hono'296const app = new Hono()297app.get('/', (c) => c.text('Bun + Hono'))298~299Bun.serve({ fetch: app.fetch, port: 3000 })300```301~302### Node.js303~304```typescript305import { serve } from '@hono/node-server'306import { Hono } from 'hono'307~308const app = new Hono()309app.get('/', (c) => c.text('Node + Hono'))310~311serve({ fetch: app.fetch, port: 3000 })312```313~314### Deno315~316```typescript317import { Hono } from 'jsr:@hono/hono'318const app = new Hono()319app.get('/', (c) => c.text('Deno + Hono'))320Deno.serve(app.fetch)321```322~323### Vercel324~325```typescript326// api/[[...route]].ts327import { Hono } from 'hono'328import { handle } from 'hono/vercel'329~330const app = new Hono().basePath('/api')331app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))332~333export const GET = handle(app)334export const POST = handle(app)335```336~337## Contoh praktikal: REST API dengan auth dan DB338~339```typescript340import { Hono } from 'hono'341import { jwt } from 'hono/jwt'342import { logger } from 'hono/logger'343import { cors } from 'hono/cors'344import { zValidator } from '@hono/zod-validator'345import { z } from 'zod'346~347type Bindings = { DB: D1Database; JWT_SECRET: string }348type Variables = { jwtPayload: { sub: string } }349~350const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()351~352app.use('*', logger())353app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))354~355const auth = (c: any, next: any) =>356 jwt({ secret: c.env.JWT_SECRET })(c, next)357~358const api = app.basePath('/api')359~360api.get('/posts', async (c) => {361 const { results } = await c.env.DB362 .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50')363 .all()364 return c.json({ posts: results })365})366~367api.post(368 '/posts',369 auth,370 zValidator('json', z.object({371 title: z.string().min(1).max(200),372 body: z.string().min(1),373 })),374 async (c) => {375 const { title, body } = c.req.valid('json')376 const userId = c.var.jwtPayload.sub377 const result = await c.env.DB378 .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id')379 .bind(title, body, userId)380 .first<{ id: number }>()381 return c.json({ id: result?.id }, 201)382 }383)384~385api.onError((err, c) => {386 console.error(err)387 return c.json({ error: 'Internal error' }, 500)388})389~390export type AppType = typeof api391export default app392```393~394## Pengujian395~396```typescript397import { describe, it, expect } from 'vitest'398import app from '../src/index'399~400describe('GET /api/posts', () => {401 it('mengembalikan senarai posts', async () => {402 const res = await app.request('/api/posts')403 expect(res.status).toBe(200)404 const body = await res.json()405 expect(body.posts).toBeInstanceOf(Array)406 })407})408```409~410## Amalan terbaik411~412### 1. Rantai definisi laluan413~414```typescript415const app = new Hono()416 .get('/posts', handler1)417 .post('/posts', handler2)418 .get('/posts/:id', handler3)419```420~421### 2. Eksport jenis, bukan implementasi422~423Klien mesti mengimport `AppType`.424~425### 3. Satu penghala bagi setiap domain426~427Sub-app untuk `posts`, `users`, `webhooks`.428~429### 4. Validasi di sempadan, sentiasa430~431Setiap input luaran mesti melalui `zValidator`.432~433### 5. Bersandar pada bindings, bukan klien global434~435Pada Cloudflare, akses KV/D1/R2 melalui `c.env`.436~437### 6. Ukur sebelum mengoptimumkan penghala438~439`SmartRouter` lalai sesuai untuk 95% kes.440~441## Kesimpulan442~443Hono telah menjadi standard de facto pada 2026 untuk membina API sedia-edge dalam TypeScript.444~445> **Senarai semak permulaan:**446>447> - [x] `npm create hono@latest` dan pilih templat runtime anda448> - [x] Tentukan laluan dengan rantaian (`.get(...).post(...)`)449> - [x] Tambah `logger`, `cors`, `secureHeaders` sebagai middleware global450> - [x] Validasi setiap input dengan `@hono/zod-validator`451> - [x] Eksport `AppType` dan gunakan API dengan klien `hc` selamat-jenis452> - [x] Tulis ujian dengan `app.request()` — tiada pelayan HTTP diperlukan453> - [x] Deploy dengan `wrangler deploy` (CF), `vercel deploy` atau bundler runtime anda454~
NORMAL · hono-framework-guide.md [readonly]454 lines · :q to close