سالها انتخاب فریمورک وب JavaScript به معنای پذیرش یک سازش بود: Express جهانی اما کند و وابسته به Node بود، Fastify سریع اما فقط Node، Next.js کامل اما سنگین. وقتی رنتایمهای edge — Cloudflare Workers، Deno Deploy، Bun، Vercel Edge — آمدند، آن فریمورکها محدودیتهای خود را نشان دادند.
Hono (در ژاپنی به معنی "شعله" 🔥) پاسخ مدرن است. فریمورکی کمتر از ۱۴KB، کاملاً روی Web Standards (Request، Response، fetch) ساخته شده، که هر جا یک رنتایم JavaScript باشد اجرا میشود. همان کد بدون تغییر روی Cloudflare Workers، Bun، Deno، Node.js، Vercel، Netlify و AWS Lambda مستقر میشود.
چرا Hono
- عملکرد.
RegExpRouterتمام الگوهای مسیر را در یک regex کامپایل میکند. بنچمارکها از ۴۰۰٫۰۰۰ ops/ثانیه فراتر میروند. - قابلیت حمل. Web Standards یعنی صفر وابستگی به Node.
- DX TypeScript-first. پارامترهای مسیر بهعنوان literal type استنتاج میشوند، کلاینت RPC امن از نظر نوع end-to-end.
شروع
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
روی Cloudflare Workers همین کافی است. روی Bun: Bun.serve({ fetch: app.fetch, port: 3000 }). روی Node: serve({ fetch: app.fetch }) از @hono/node-server.
مسیریابی
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) })
گروهبندی مسیرها
// 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)
شیء 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 }) })
میانافزار: مدل پیازی
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`) })
میانافزارهای داخلی
| میانافزار | کاربرد |
|---|---|
logger | لاگ ساختاریافته متد، مسیر، وضعیت، مدت |
cors | CORS قابل پیکربندی بر اساس origin، متدها، هدرها |
csrf | محافظت CSRF مبتنی بر origin |
secureHeaders | تنظیم CSP، HSTS، X-Frame-Options |
bearerAuth / basicAuth | احراز هویت Bearer/Basic آماده |
jwt | تأیید/امضای JWT با jose |
etag | تولید ETag و مدیریت ۳۰۴ |
cache | کش از طریق Web Cache API |
compress | gzip/deflate روی پاسخ |
bodyLimit | رد body بالاتر از آستانه |
timing | هدر Server-Timing برای پروفایلینگ |
میانافزار سفارشی امن از نظر نوع
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 }) })
اعتبارسنجی با 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
// 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: 'سلام', body: 'Hono یک شعله است' }, })
تمایز کد وضعیت
.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() }
روترها و عملکرد
| روتر | نقاط قوت | چه زمانی استفاده شود |
|---|---|---|
RegExpRouter | حداکثر سرعت، regex کامپایلشده | پیشفرض برای اکثر APIها |
TrieRouter | از همه الگوها پشتیبانی میکند | الگوهای پیچیدهای که RegExp مدیریت نمیکند |
SmartRouter | بهترین را خودکار انتخاب میکند | پیشفرض پیشنهادی |
LinearRouter | ثبت فوقسریع | ورکرهای یکبار مصرف، cold start حیاتی |
PatternRouter | حداقل بسته (<15KB) | محدودیتهای شدید اندازه |
import { Hono } from 'hono' import { LinearRouter } from 'hono/router/linear-router' const app = new Hono({ router: new LinearRouter() })
استقرار چندرنتایم
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
استقرار: 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)
مثال عملی: REST API با احراز هویت و 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
تست
import { describe, it, expect } from 'vitest' import app from '../src/index' describe('GET /api/posts', () => { it('فهرست پستها را برمیگرداند', async () => { const res = await app.request('/api/posts') expect(res.status).toBe(200) const body = await res.json() expect(body.posts).toBeInstanceOf(Array) }) })
بهترین شیوهها
۱. تعاریف مسیر را زنجیرهای کنید
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
۲. نوع را خروجی بدهید، نه پیادهسازی را
کلاینت باید AppType را وارد کند.
۳. یک روتر برای هر دامنه
زیراپ برای posts، users، webhooks.
۴. اعتبارسنجی در مرز، همیشه
هر ورودی خارجی باید از zValidator عبور کند.
۵. به bindings تکیه کنید، نه به کلاینتهای جهانی
در Cloudflare از طریق c.env به KV/D1/R2 دسترسی پیدا کنید.
۶. قبل از بهینهسازی روتر اندازهگیری کنید
SmartRouter پیشفرض برای ۹۵٪ موارد مناسب است.
نتیجهگیری
Hono در سال ۲۰۲۶ به استاندارد عملی برای ساخت APIهای آماده edge در TypeScript تبدیل شده است.
چکلیست شروع:
npm create hono@latestو قالب رنتایم خود را انتخاب کنید- مسیرها را با زنجیره تعریف کنید (
.get(...).post(...))logger،cors،secureHeadersرا بهعنوان میانافزار جهانی اضافه کنید- هر ورودی را با
@hono/zod-validatorاعتبارسنجی کنیدAppTypeرا خروجی دهید و API را با کلاینتhcامن از نظر نوع مصرف کنید- تستها را با
app.request()بنویسید — بدون نیاز به سرور HTTP- با
wrangler deploy(CF)،vercel deployیا باندلر رنتایم خود مستقر کنید