Sa loob ng maraming taon, ang pagpili ng JavaScript web framework ay nangangahulugang pagtanggap ng trade-off: ang Express ay universal pero mabagal at nakatali sa Node, ang Fastify ay mabilis pero Node lang, ang Next.js ay full-featured pero mabigat. Nang dumating ang edge runtimes โ Cloudflare Workers, Deno Deploy, Bun, Vercel Edge โ ipinakita ng mga framework na iyon ang kanilang mga limitasyon.
Ang Hono (Hapon para sa "apoy" ๐ฅ) ay ang modernong sagot. Isang framework na mababa sa 14KB, ganap na nakabase sa Web Standards (Request, Response, fetch), na tumatakbo saanman may JavaScript runtime. Ang parehong code ay nade-deploy sa Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify at AWS Lambda โ nang walang pagbabago.
Bakit Hono
- Performance. Pinagsasama-sama ng
RegExpRouterang lahat ng route patterns sa isang regex. Lumalampas ang benchmarks sa 400,000 ops/s. - Portability. Web Standards ay nangangahulugang zero Node dependencies.
- TypeScript-first DX. Ang path parameters ay na-i-infer bilang literal types, end-to-end type-safe RPC client.
Pagsisimula
npm create hono@latest my-api cd my-api npm install npm run dev
// src/index.ts import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hello Hono!')) export default app
Sa Cloudflare Workers, sapat na ito. Sa Bun: Bun.serve({ fetch: app.fetch, port: 3000 }). Sa Node: serve({ fetch: app.fetch }) mula sa @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') 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) })
Pag-grupo ng routes
// 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)
Ang Context object
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 }) })
Middleware: ang onion model
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`) })
Built-in middleware
| Middleware | Layunin |
|---|---|
logger | Structured logs ng method, path, status, duration |
cors | CORS na configurable ayon sa origin, methods, headers |
csrf | Origin-based CSRF protection |
secureHeaders | Nagse-set ng CSP, HSTS, X-Frame-Options |
bearerAuth / basicAuth | Out-of-the-box Bearer/Basic auth |
jwt | JWT verify/sign gamit ang jose |
etag | Bumubuo ng ETag at hinahawakan ang 304 |
cache | Cache sa pamamagitan ng Web Cache API |
compress | gzip/deflate sa response |
bodyLimit | Tinatanggihan ang bodies na lampas sa threshold |
timing | Server-Timing header para sa profiling |
Type-safe custom middleware
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 }) })
Validation gamit ang 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: end-to-end type-safe client
// 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: 'Kumusta', body: 'Ang Hono ay isang apoy' }, })
Status code discrimination
.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() }
Routers at performance
| Router | Lakas | Kailan gamitin |
|---|---|---|
RegExpRouter | Pinakamabilis, compiled regex | Default para sa karamihan ng APIs |
TrieRouter | Sumusuporta sa lahat ng patterns | Kumplikadong patterns na hindi kayang hawakan ng RegExp |
SmartRouter | Awtomatikong pumipili ng pinakamahusay | Inirerekomendang default |
LinearRouter | Ultra-bilis na registration | One-shot workers, kritikal na cold start |
PatternRouter | Pinakamaliit na bundle (<15KB) | Sukdulang limitasyon sa laki |
import { Hono } from 'hono' import { LinearRouter } from 'hono/router/linear-router' const app = new Hono({ router: new LinearRouter() })
Multi-runtime deploy
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)
Praktikal na halimbawa: REST API na may auth at 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
Pagsubok
import { describe, it, expect } from 'vitest' import app from '../src/index' describe('GET /api/posts', () => { it('nagbabalik ng listahan ng mga post', async () => { const res = await app.request('/api/posts') expect(res.status).toBe(200) const body = await res.json() expect(body.posts).toBeInstanceOf(Array) }) })
Best practices
1. I-chain ang mga route definition
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
2. I-export ang type, hindi ang implementation
Ang client ay dapat mag-import ng AppType.
3. Isang router bawat domain
Sub-app para sa posts, users, webhooks.
4. Validation sa gilid, palagi
Bawat external input ay dapat dumaan sa zValidator.
5. Umasa sa bindings, hindi sa global clients
Sa Cloudflare, i-access ang KV/D1/R2 sa pamamagitan ng c.env.
6. Sukatin bago i-optimize ang router
Ang default na SmartRouter ay angkop sa 95% ng mga kaso.
Konklusyon
Naging de facto standard ang Hono noong 2026 para sa pagbuo ng edge-ready APIs sa TypeScript.
Checklist sa pagsisimula:
npm create hono@latestat piliin ang iyong runtime template- I-define ang routes na may chaining (
.get(...).post(...))- Idagdag ang
logger,cors,secureHeadersbilang global middleware- I-validate ang bawat input gamit ang
@hono/zod-validator- I-export ang
AppTypeat konsumihin ang API gamit ang type-safehcclient- Sumulat ng tests gamit ang
app.request()โ walang HTTP server na kailangan- I-deploy gamit ang
wrangler deploy(CF),vercel deployo ang bundler ng iyong runtime