spinny:~/writing $ vim hono-framework-guide.md
1~2Selama bertahun-tahun, memilih framework web JavaScript berarti menerima trade-off: Express universal tapi lambat dan terikat Node, Fastify cepat tapi hanya Node, Next.js lengkap tapi berat. Saat runtime edge — Cloudflare Workers, Deno Deploy, Bun, Vercel Edge — muncul, framework-framework itu menunjukkan batasannya: dependensi tidak kompatibel, bundle besar, API terikat pada `req`/`res` Node.3~4[Hono](https://hono.dev) (Jepang untuk "api" 🔥) adalah jawaban modern. Framework di bawah 14KB, sepenuhnya dibangun di atas Web Standards (`Request`, `Response`, `fetch`), berjalan di mana saja ada runtime JavaScript. Kode yang sama dideploy ke Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify, dan AWS Lambda — tanpa perubahan.5~6## Mengapa Hono7~8Hono melakukan tiga hal lebih baik dari siapa pun:9~101. **Performa.** `RegExpRouter` mengompilasi semua pola route ke satu regex, menghindari loop linear router tradisional. Benchmark melampaui 400.000 ops/s.112. **Portabilitas.** Web Standards berarti nol dependensi Node. `app.fetch` yang sama diekspor sebagai default di Cloudflare Worker, dilewatkan ke `Bun.serve`, dimount di server Deno, atau diadaptasi dengan `@hono/node-server`.123. **DX TypeScript-first.** Path parameter diinferensi sebagai literal type, klien RPC type-safe end-to-end, validator yang menginferensi tipe input dan output.13~14```mermaid15graph LR16 Client[Client] -->|Request| App[app.fetch]17 App --> MW1[Middleware 1]18 MW1 --> MW2[Middleware 2]19 MW2 --> Router[RegExpRouter]20 Router --> Handler[Route Handler]21 Handler --> Context[c.json / c.text]22 Context -->|Response| Client23 App -.->|deploy| CF[Cloudflare Workers]24 App -.->|deploy| Bun[Bun]25 App -.->|deploy| Deno[Deno]26 App -.->|deploy| Node[Node.js]27 App -.->|deploy| Vercel[Vercel]28```29~30## Memulai31~32```bash33npm create hono@latest my-api34cd my-api35npm install36npm run dev37```38~39Starter menanyakan template mana: `cloudflare-workers`, `bun`, `deno`, `nodejs`, `vercel`, `aws-lambda`, `nextjs` dan lainnya. Untuk uji coba cepat, bisa juga mulai dari satu file:40~41```typescript42// src/index.ts43import { Hono } from 'hono'44~45const app = new Hono()46~47app.get('/', (c) => c.text('Hello Hono!'))48~49export default app50```51~52Di Cloudflare Workers itu cukup. Di Bun: `Bun.serve({ fetch: app.fetch, port: 3000 })`. Di Node: `serve({ fetch: app.fetch })` dari `@hono/node-server`.53~54## Routing55~56```typescript57import { Hono } from 'hono'58~59const app = new Hono()60~61app.get('/', (c) => c.text('Home'))62app.get('/posts/:id', (c) => {63 const id = c.req.param('id') // string, bertipe64 return c.json({ id })65})66app.get('/posts/:id/comments/:commentId', (c) => {67 const { id, commentId } = c.req.param()68 return c.json({ id, commentId })69})70app.get('/files/*', (c) => c.text('Wildcard'))71app.post('/posts', async (c) => {72 const body = await c.req.json()73 return c.json({ created: body }, 201)74})75```76~77Parameter diinferensi sebagai literal type: TypeScript tahu `c.req.param('id')` mengembalikan `string` hanya jika Anda mendeklarasikan `:id` di pola. Salah ketik adalah error compile-time.78~79### Mengelompokkan rute80~81```typescript82// routes/posts.ts83import { Hono } from 'hono'84~85const posts = new Hono()86posts.get('/', (c) => c.json({ posts: [] }))87posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))88export default posts89~90// src/index.ts91import { Hono } from 'hono'92import posts from './routes/posts'93~94const app = new Hono()95app.route('/posts', posts)96```97~98## Objek Context99~100```typescript101app.post('/echo', async (c) => {102 const userAgent = c.req.header('User-Agent')103 const page = c.req.query('page')104 const body = await c.req.json()105 const env = c.env106~107 c.set('requestId', crypto.randomUUID())108 const id = c.get('requestId')109~110 c.header('X-Request-Id', id)111 c.status(200)112 return c.json({ userAgent, page, body, id })113})114```115~116Metode response utama: `c.text`, `c.json`, `c.html`, `c.body` (raw), `c.redirect`.117~118## Middleware: model bawang119~120```typescript121import { Hono } from 'hono'122import { logger } from 'hono/logger'123import { cors } from 'hono/cors'124import { secureHeaders } from 'hono/secure-headers'125~126const app = new Hono()127~128app.use('*', logger())129app.use('*', secureHeaders())130app.use('/api/*', cors({ origin: 'https://spinny.dev' }))131~132app.use('*', async (c, next) => {133 const start = performance.now()134 await next()135 c.header('X-Response-Time', `${performance.now() - start}ms`)136})137```138~139`await next()` menyerahkan kontrol ke middleware berikutnya.140~141### Middleware bawaan142~143| Middleware | Fungsi |144|-----------|---------|145| `logger` | Log terstruktur metode, path, status, durasi |146| `cors` | CORS dapat dikonfigurasi per origin, metode, header |147| `csrf` | Proteksi CSRF berbasis origin |148| `secureHeaders` | Mengatur CSP, HSTS, X-Frame-Options |149| `bearerAuth` / `basicAuth` | Auth Bearer/Basic siap pakai |150| `jwt` | Verify/sign JWT dengan `jose` |151| `etag` | Membuat ETag dan menangani 304 |152| `cache` | Cache via Web Cache API |153| `compress` | gzip/deflate respons |154| `bodyLimit` | Menolak body di atas ambang |155| `timing` | Header Server-Timing untuk profiling |156~157### Middleware kustom type-safe158~159```typescript160import { createMiddleware } from 'hono/factory'161~162type AuthVars = { userId: string; role: 'user' | 'admin' }163~164export const requireAuth = createMiddleware<{ Variables: AuthVars }>(165 async (c, next) => {166 const token = c.req.header('Authorization')?.replace('Bearer ', '')167 if (!token) return c.json({ error: 'Unauthorized' }, 401)168~169 const payload = await verifyJwt(token)170 c.set('userId', payload.sub)171 c.set('role', payload.role)172 await next()173 }174)175~176app.get('/me', requireAuth, (c) => {177 const userId = c.var.userId178 return c.json({ userId })179})180```181~182## Validasi dengan Zod183~184```typescript185import { Hono } from 'hono'186import { zValidator } from '@hono/zod-validator'187import { z } from 'zod'188~189const createPost = z.object({190 title: z.string().min(1).max(200),191 body: z.string().min(1),192 tags: z.array(z.string()).default([]),193})194~195app.post(196 '/posts',197 zValidator('json', createPost),198 (c) => {199 const data = c.req.valid('json')200 return c.json({ ok: true, post: data }, 201)201 }202)203```204~205## RPC: klien type-safe end-to-end206~207```typescript208// server.ts209import { Hono } from 'hono'210import { zValidator } from '@hono/zod-validator'211import { z } from 'zod'212~213const app = new Hono()214 .get('/posts/:id', (c) =>215 c.json({ id: c.req.param('id'), title: 'Hello' })216 )217 .post(218 '/posts',219 zValidator('json', z.object({ title: z.string(), body: z.string() })),220 (c) => c.json({ ok: true }, 201)221 )222~223export type AppType = typeof app224export default app225```226~227```typescript228// client.ts229import { hc } from 'hono/client'230import type { AppType } from './server'231~232const client = hc<AppType>('https://api.spinny.dev')233~234const res = await client.posts[':id'].$get({ param: { id: '42' } })235if (res.ok) {236 const data = await res.json()237 console.log(data.title)238}239~240const created = await client.posts.$post({241 json: { title: 'Halo', body: 'Hono adalah api' },242})243```244~245### Diskriminasi status code246~247```typescript248.get('/posts/:id', (c) => {249 const post = findPost(c.req.param('id'))250 if (!post) return c.json({ error: 'not found' }, 404)251 return c.json({ post }, 200)252})253```254~255```typescript256const res = await client.posts[':id'].$get({ param: { id } })257if (res.status === 404) {258 const { error } = await res.json()259}260if (res.status === 200) {261 const { post } = await res.json()262}263```264~265## Router dan performa266~267| Router | Kekuatan | Kapan digunakan |268|--------|-----------|-------------|269| `RegExpRouter` | Kecepatan maksimal, regex terkompilasi | Default untuk sebagian besar API |270| `TrieRouter` | Mendukung semua pola | Pola kompleks yang tidak ditangani RegExp |271| `SmartRouter` | Memilih yang terbaik otomatis | Default yang direkomendasikan |272| `LinearRouter` | Registrasi sangat cepat | Worker one-shot, cold start kritis |273| `PatternRouter` | Bundle minimal (<15KB) | Batasan ukuran ekstrem |274~275```typescript276import { Hono } from 'hono'277import { LinearRouter } from 'hono/router/linear-router'278~279const app = new Hono({ router: new LinearRouter() })280```281~282## Deploy multi-runtime283~284### Cloudflare Workers285~286```typescript287import { Hono } from 'hono'288~289type Bindings = { MY_KV: KVNamespace; DB: D1Database }290const app = new Hono<{ Bindings: Bindings }>()291~292app.get('/cache/:key', async (c) => {293 const value = await c.env.MY_KV.get(c.req.param('key'))294 return c.json({ value })295})296~297export default app298```299~300Deploy: `npx wrangler deploy`.301~302### Bun303~304```typescript305import { Hono } from 'hono'306const app = new Hono()307app.get('/', (c) => c.text('Bun + Hono'))308~309Bun.serve({ fetch: app.fetch, port: 3000 })310```311~312### Node.js313~314```typescript315import { serve } from '@hono/node-server'316import { Hono } from 'hono'317~318const app = new Hono()319app.get('/', (c) => c.text('Node + Hono'))320~321serve({ fetch: app.fetch, port: 3000 })322```323~324### Deno325~326```typescript327import { Hono } from 'jsr:@hono/hono'328const app = new Hono()329app.get('/', (c) => c.text('Deno + Hono'))330Deno.serve(app.fetch)331```332~333### Vercel334~335```typescript336// api/[[...route]].ts337import { Hono } from 'hono'338import { handle } from 'hono/vercel'339~340const app = new Hono().basePath('/api')341app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))342~343export const GET = handle(app)344export const POST = handle(app)345```346~347## Contoh praktis: REST API dengan auth dan DB348~349```typescript350import { Hono } from 'hono'351import { jwt } from 'hono/jwt'352import { logger } from 'hono/logger'353import { cors } from 'hono/cors'354import { zValidator } from '@hono/zod-validator'355import { z } from 'zod'356~357type Bindings = { DB: D1Database; JWT_SECRET: string }358type Variables = { jwtPayload: { sub: string } }359~360const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()361~362app.use('*', logger())363app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))364~365const auth = (c: any, next: any) =>366 jwt({ secret: c.env.JWT_SECRET })(c, next)367~368const api = app.basePath('/api')369~370api.get('/posts', async (c) => {371 const { results } = await c.env.DB372 .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50')373 .all()374 return c.json({ posts: results })375})376~377api.post(378 '/posts',379 auth,380 zValidator('json', z.object({381 title: z.string().min(1).max(200),382 body: z.string().min(1),383 })),384 async (c) => {385 const { title, body } = c.req.valid('json')386 const userId = c.var.jwtPayload.sub387 const result = await c.env.DB388 .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id')389 .bind(title, body, userId)390 .first<{ id: number }>()391 return c.json({ id: result?.id }, 201)392 }393)394~395api.onError((err, c) => {396 console.error(err)397 return c.json({ error: 'Internal error' }, 500)398})399~400export type AppType = typeof api401export default app402```403~404## Testing405~406```typescript407import { describe, it, expect } from 'vitest'408import app from '../src/index'409~410describe('GET /api/posts', () => {411 it('mengembalikan daftar post', async () => {412 const res = await app.request('/api/posts')413 expect(res.status).toBe(200)414 const body = await res.json()415 expect(body.posts).toBeInstanceOf(Array)416 })417})418```419~420## Praktik terbaik421~422### 1. Rangkai definisi rute423~424```typescript425const app = new Hono()426 .get('/posts', handler1)427 .post('/posts', handler2)428 .get('/posts/:id', handler3)429```430~431### 2. Ekspor tipe, bukan implementasi432~433Klien harus mengimpor `AppType`. `import type` memastikan build frontend tidak menyertakan kode backend.434~435### 3. Satu router per domain436~437Sub-app untuk `posts`, `users`, `webhooks`. Komposisi via `app.route()`.438~439### 4. Validasi di tepi, selalu440~441Setiap input eksternal harus melewati `zValidator`.442~443### 5. Bersandar pada bindings, bukan klien global444~445Di Cloudflare, akses KV/D1/R2 via `c.env`.446~447### 6. Ukur sebelum mengoptimalkan router448~449`SmartRouter` default cocok untuk 95% kasus.450~451## Kesimpulan452~453Hono telah menjadi standar de facto pada 2026 untuk membangun API edge-ready dengan TypeScript. Kombinasi Web Standards, performa, type safety, dan portabilitas memecahkan tepat masalah-masalah yang menahan framework tradisional.454~455> **Checklist memulai:**456>457> - [x] `npm create hono@latest` dan pilih template runtime Anda458> - [x] Definisikan rute dengan chaining (`.get(...).post(...)`)459> - [x] Tambahkan `logger`, `cors`, `secureHeaders` sebagai middleware global460> - [x] Validasi setiap input dengan `@hono/zod-validator`461> - [x] Ekspor `AppType` dan konsumsi API dengan klien `hc` type-safe462> - [x] Tulis tes dengan `app.request()` — tanpa server HTTP463> - [x] Deploy dengan `wrangler deploy` (CF), `vercel deploy`, atau bundler runtime Anda464~
NORMAL · hono-framework-guide.md [readonly]464 lines · :q to close