Selama 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.
Hono (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.
Mengapa Hono
Hono melakukan tiga hal lebih baik dari siapa pun:
- Performa.
RegExpRoutermengompilasi semua pola route ke satu regex, menghindari loop linear router tradisional. Benchmark melampaui 400.000 ops/s. - Portabilitas. Web Standards berarti nol dependensi Node.
app.fetchyang sama diekspor sebagai default di Cloudflare Worker, dilewatkan keBun.serve, dimount di server Deno, atau diadaptasi dengan@hono/node-server. - DX TypeScript-first. Path parameter diinferensi sebagai literal type, klien RPC type-safe end-to-end, validator yang menginferensi tipe input dan output.
Memulai
npm create hono@latest my-api cd my-api npm install npm run dev
Starter menanyakan template mana: cloudflare-workers, bun, deno, nodejs, vercel, aws-lambda, nextjs dan lainnya. Untuk uji coba cepat, bisa juga mulai dari satu file:
// src/index.ts import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hello Hono!')) export default app
Di Cloudflare Workers itu cukup. Di Bun: Bun.serve({ fetch: app.fetch, port: 3000 }). Di Node: serve({ fetch: app.fetch }) dari @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') // string, bertipe 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) })
Parameter 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.
Mengelompokkan rute
// 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)
Objek 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 }) })
Metode response utama: c.text, c.json, c.html, c.body (raw), c.redirect.
Middleware: model bawang
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`) })
await next() menyerahkan kontrol ke middleware berikutnya.
Middleware bawaan
| Middleware | Fungsi |
|---|---|
logger | Log terstruktur metode, path, status, durasi |
cors | CORS dapat dikonfigurasi per origin, metode, header |
csrf | Proteksi CSRF berbasis origin |
secureHeaders | Mengatur CSP, HSTS, X-Frame-Options |
bearerAuth / basicAuth | Auth Bearer/Basic siap pakai |
jwt | Verify/sign JWT dengan jose |
etag | Membuat ETag dan menangani 304 |
cache | Cache via Web Cache API |
compress | gzip/deflate respons |
bodyLimit | Menolak body di atas ambang |
timing | Header Server-Timing untuk profiling |
Middleware kustom 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 }) })
Validasi dengan 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: klien 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: 'Halo', body: 'Hono adalah api' }, })
Diskriminasi 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 dan performa
| Router | Kekuatan | Kapan digunakan |
|---|---|---|
RegExpRouter | Kecepatan maksimal, regex terkompilasi | Default untuk sebagian besar API |
TrieRouter | Mendukung semua pola | Pola kompleks yang tidak ditangani RegExp |
SmartRouter | Memilih yang terbaik otomatis | Default yang direkomendasikan |
LinearRouter | Registrasi sangat cepat | Worker one-shot, cold start kritis |
PatternRouter | Bundle minimal (<15KB) | Batasan ukuran ekstrem |
import { Hono } from 'hono' import { LinearRouter } from 'hono/router/linear-router' const app = new Hono({ router: new LinearRouter() })
Deploy multi-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)
Contoh praktis: REST API dengan auth dan 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('mengembalikan daftar post', async () => { const res = await app.request('/api/posts') expect(res.status).toBe(200) const body = await res.json() expect(body.posts).toBeInstanceOf(Array) }) })
Praktik terbaik
1. Rangkai definisi rute
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
2. Ekspor tipe, bukan implementasi
Klien harus mengimpor AppType. import type memastikan build frontend tidak menyertakan kode backend.
3. Satu router per domain
Sub-app untuk posts, users, webhooks. Komposisi via app.route().
4. Validasi di tepi, selalu
Setiap input eksternal harus melewati zValidator.
5. Bersandar pada bindings, bukan klien global
Di Cloudflare, akses KV/D1/R2 via c.env.
6. Ukur sebelum mengoptimalkan router
SmartRouter default cocok untuk 95% kasus.
Kesimpulan
Hono 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.
Checklist memulai:
npm create hono@latestdan pilih template runtime Anda- Definisikan rute dengan chaining (
.get(...).post(...))- Tambahkan
logger,cors,secureHeaderssebagai middleware global- Validasi setiap input dengan
@hono/zod-validator- Ekspor
AppTypedan konsumsi API dengan klienhctype-safe- Tulis tes dengan
app.request()โ tanpa server HTTP- Deploy dengan
wrangler deploy(CF),vercel deploy, atau bundler runtime Anda