لسنوات، كان اختيار إطار ويب JavaScript يعني قبول مقايضة: Express عام لكنه بطيء ومرتبط بـ Node، و Fastify سريع لكنه فقط لـ Node، و Next.js كامل لكنه ثقيل. عندما ظهرت بيئات تشغيل الحافة — Cloudflare Workers و Deno Deploy و Bun و Vercel Edge — أظهرت تلك الأطر حدودها: تبعيات غير متوافقة، حزم ضخمة، APIs مرتبطة بـ req/res الخاصة بـ Node.
Hono (تعني "اللهب" 🔥 بالـيابانية) هي الإجابة الحديثة. إطار يقل حجمه عن 14KB، مبني بالكامل على معايير الويب (Request، Response، fetch)، ويعمل في أي مكان توجد فيه بيئة تشغيل JavaScript. نفس الكود يُنشر على Cloudflare Workers و Bun و Deno و Node.js و Vercel و Netlify و AWS Lambda — دون تغيير.
لماذا Hono
يقوم Hono بثلاثة أشياء أفضل من أي شخص آخر:
- الأداء. يقوم
RegExpRouterبتجميع كل أنماط التوجيه في تعبير منتظم واحد، متجنباً الحلقات الخطية للموجهات التقليدية. تتجاوز المعايير 400000 عملية في الثانية، مما يضع Hono ضمن أسرع الموجهات في نظام JavaScript البيئي. - قابلية النقل. معايير الويب تعني صفر تبعيات على Node. نفس
app.fetchيُصدَّر افتراضياً في Cloudflare Worker، ويُمرَّر إلىBun.serve، ويُحمَّل في خادم Deno، أو يُكيَّف مع@hono/node-server. - DX يعتمد TypeScript أولاً. معاملات المسار مستنتجة كأنواع حرفية، عميل RPC آمن نوعياً من طرف لطرف، مدققات تستنتج أنواع المدخلات والمخرجات.
البدء
أسرع طريقة هي المُهيِّئ الرسمي، الذي ينشئ المشروع لبيئة التشغيل المختارة.
npm create hono@latest my-api cd my-api npm install npm run dev
يسأل المُهيِّئ عن أي قالب: cloudflare-workers، bun، deno، nodejs، vercel، aws-lambda، nextjs وغيرها. للتجربة السريعة، يمكنك أيضاً البدء من ملف واحد:
// 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. السطح متطابق.
التوجيه
تُعلَن المسارات بطرق فعل HTTP وتدعم المعاملات والأحرف البديلة والتعبيرات المنتظمة.
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، مكتوب نوعياً 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) })
تُستنتج المعاملات كأنواع حرفية: يعرف TypeScript أن c.req.param('id') يُعيد string فقط إذا أعلنت :id في النمط. الخطأ في التهجئة هو خطأ وقت التجميع.
تجميع المسارات
app.route() يسمح بتركيب التطبيقات الفرعية كوحدات، كل واحدة بادئتها الخاصة.
// 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)
ترث المسارات المتداخلة المسار الأساسي ونوع تطبيق الجذر، لذلك يرى عميل RPC البنية كاملة.
كائن Context
كل معالج يستقبل c — Context للطلب الحالي. هو الـ API الوحيد الذي تحتاج تعلمه لقراءة المدخلات وإنتاج المخرجات.
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 // الروابط (KV، D1، الأسرار على Cloudflare) // متغيرات مشتركة بين الوسيط والمعالج 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 }) })
طرق الاستجابة الرئيسية هي c.text و c.json و c.html و c.body (raw) و c.redirect. كلها تقبل رمز حالة كوسيطة ثانية.
الوسطاء: نموذج البصلة
الوسطاء هي دوال (c, next) => ... يمكنها تشغيل كود قبل المعالج وبعده. عند تركيبها تشكل نموذج البصلة الكلاسيكي: أول وسيط مسجل هو أول من يبدأ وآخر من ينتهي.
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() يُسلِّم التحكم للوسيط التالي. كل ما بعده يعمل في مرحلة الاستجابة، بعد أن ينتج المعالج نتيجة. إعادة Response قبل next() يقصر السلسلة.
الوسطاء المدمجون
| الوسيط | الغرض |
|---|---|
logger | سجلات منظمة للطريقة، المسار، الحالة، المدة |
cors | CORS قابل للتكوين بحسب الأصل، الطرق، الترويسات |
csrf | حماية CSRF قائمة على الأصل |
secureHeaders | يضبط CSP، HSTS، X-Frame-Options |
bearerAuth / basicAuth | مصادقة Bearer/Basic جاهزة |
jwt | تحقق وتوقيع JWT بـ jose |
etag | يولد ETag ويعالج 304 |
cache | تخزين مؤقت عبر Web Cache API |
compress | gzip/deflate على الاستجابة |
bodyLimit | يرفض الأجسام فوق الحد |
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 // string، مكتوب نوعياً 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) } )
إذا فشل الجسم في التحقق، يستجيب Hono بـ 400 مع خطأ Zod قبل حتى استدعاء المعالج. يمكن أيضاً التحقق من query و param و header و cookie و form.
RPC: عميل آمن نوعياً من طرف لطرف
// 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 لهب' }, })
أعد تسمية مسار على الخادم وسيُكسر TypeScript الخاص بالعميل فوراً في CI. نفس ميزة tRPC، لكن عبر HTTP المعياري، دون وسطاء محددين، ومع حزمة صغيرة جداً.
تمييز رموز الحالة
.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() // { error: string } } if (res.status === 200) { const { post } = await res.json() // { post: Post } }
الموجهات والأداء
| الموجه | نقاط القوة | متى يستخدم |
|---|---|---|
RegExpRouter | أعلى سرعة، regex مجمع | افتراضي لمعظم APIs |
TrieRouter | يدعم كل الأنماط | الأنماط المعقدة التي لا يتعامل معها RegExp |
SmartRouter | يختار الأفضل تلقائياً | الافتراضي الموصى به |
LinearRouter | تسجيل فائق السرعة | عمال one-shot، البدء البارد حرج |
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 مع المصادقة وقاعدة البيانات
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
عميل React يستهلكها بأمان نوعي كامل:
import { hc } from 'hono/client' import type { AppType } from '../api/src/index' const api = hc<AppType>(import.meta.env.VITE_API_URL) const res = await api.posts.$post({ json: { title: 'Hono في 2026', body: '...' }, }, { headers: { Authorization: `Bearer ${token}` }, })
الاختبار
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) }) })
app.request() يسمح باختبار المسارات دون تشغيل خادم HTTP.
أفضل الممارسات
1. سلسلة تعريفات المسارات
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
السلسلة تحافظ على نوع app محدثاً مع كل مسار، أمر جوهري لعميل RPC.
2. صدّر النوع، لا التطبيق
يجب أن يستورد العميل AppType لا التطبيق. import type يضمن عدم تضمين كود الواجهة الخلفية في بناء الواجهة الأمامية.
3. موجه واحد لكل مجال
تطبيق فرعي لـ posts، آخر لـ users، آخر لـ webhooks. ركّبهم بـ app.route().
4. التحقق على الحافة، دائماً
كل مدخل خارجي (body، query، header) يجب أن يمر عبر zValidator.
5. اعتمد على الروابط، لا على العملاء العالميين
على Cloudflare، الوصول لـ KV/D1/R2 عبر c.env.
6. قِس قبل تحسين الموجه
SmartRouter الافتراضي مناسب لـ 95٪ من الحالات.
الخلاصة
أصبح Hono في 2026 المعيار الفعلي لبناء APIs جاهزة للحافة بـ TypeScript. الجمع بين معايير الويب والأداء والأمان النوعي وقابلية النقل يحل بالضبط المشكلات التي أعاقت الأطر التقليدية.
قائمة التحقق للبدء:
npm create hono@latestواختر قالب بيئة التشغيل- عرّف المسارات بالسلسلة (
.get(...).post(...))- أضف
logger،cors،secureHeadersكوسطاء عامين- تحقق من كل مدخل بـ
@hono/zod-validator- صدّر
AppTypeواستهلك الـ API بعميلhcالآمن نوعياً- اكتب الاختبارات بـ
app.request()— دون خادم HTTP- انشر بـ
wrangler deploy(CF) أوvercel deployأو حزمة بيئتك