بناء واجهات REST API باستخدام Hono و Bun: البديل العصري لـ Express

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

بناء واجهات REST API باستخدام Hono و Bun

شهدت منظومة JavaScript تطوراً هائلاً في السنوات الأخيرة. بينما خدمنا Express.js بشكل ممتاز لأكثر من عقد، ظهرت بدائل حديثة تقدم أداءً أفضل وأماناً للأنواع وتجربة مطور متميزة. في هذا الدرس الشامل، سنستكشف Hono - إطار عمل فائق السرعة - مع Bun - بيئة تشغيل JavaScript السريعة.

لماذا Hono مع Bun؟

قبل الغوص في الكود، دعنا نفهم سبب انتشار هذا الثنائي بسرعة:

مميزات بيئة Bun

  • السرعة: أسرع بـ 4 مرات من Node.js في كثير من العمليات
  • TypeScript مدمج: لا حاجة لأي ترجمة
  • مجمّع مدمج: لا حاجة لـ webpack أو esbuild
  • SQLite مدمج: قاعدة بيانات جاهزة للاستخدام
  • متوافق مع npm: استخدم حزمك المفضلة

مميزات إطار Hono

  • خفيف جداً: حوالي 14 كيلوبايت فقط، بدون اعتماديات
  • متعدد البيئات: يعمل على Bun و Node و Deno و Cloudflare Workers
  • آمن للأنواع: دعم كامل لـ TypeScript
  • واجهة حديثة: Middleware والتوجيه والتحقق مدمجة
  • معايير الويب: يستخدم Fetch API و Request/Response

المتطلبات الأساسية

قبل البدء، تأكد من:

  • معرفة أساسية بـ JavaScript/TypeScript
  • إلمام بمفاهيم REST API
  • راحة في استخدام سطر الأوامر
  • محرر كود (VS Code موصى به)

يستخدم هذا الدرس Bun 1.1+ و Hono 4.x. قد تختلف بعض الأوامر مع إصدارات مختلفة.

الخطوة 1: تثبيت Bun

إذا لم تثبّت Bun بعد، فالأمر بسيط:

# macOS و Linux
curl -fsSL https://bun.sh/install | bash
 
# Windows (باستخدام PowerShell)
powershell -c "irm bun.sh/install.ps1 | iex"

تحقق من التثبيت:

bun --version
# يجب أن يظهر: 1.1.x أو أعلى

الخطوة 2: إنشاء المشروع

لنبدأ بإنشاء مشروع جديد:

# إنشاء مجلد المشروع
mkdir hono-api && cd hono-api
 
# تهيئة المشروع مع Bun
bun init -y
 
# تثبيت Hono
bun add hono
 
# تثبيت اعتماديات التطوير
bun add -d @types/bun

يجب أن تكون بنية المشروع:

hono-api/
├── node_modules/
├── package.json
├── tsconfig.json
├── bun.lockb
└── index.ts

الخطوة 3: أول خادم Hono

استبدل محتوى index.ts بـ:

import { Hono } from 'hono'
 
const app = new Hono()
 
// المسار الرئيسي
app.get('/', (c) => {
  return c.json({
    message: 'مرحباً بك في Hono API!',
    version: '1.0.0',
    timestamp: new Date().toISOString()
  })
})
 
// نقطة فحص الصحة
app.get('/health', (c) => {
  return c.json({ status: 'healthy' })
})
 
// تشغيل الخادم
export default {
  port: 3000,
  fetch: app.fetch
}

شغّل الخادم:

bun run index.ts

زر http://localhost:3000 - يجب أن ترى الاستجابة بصيغة JSON!

يدعم Bun إعادة التحميل التلقائي. استخدم bun --watch index.ts أثناء التطوير لإعادة التشغيل التلقائي.

الخطوة 4: تنظيم بنية الـ API

لـ API جاهز للإنتاج، لننظم الكود بشكل صحيح. أنشئ هذه البنية:

hono-api/
├── src/
│   ├── index.ts
│   ├── routes/
│   │   ├── index.ts
│   │   ├── users.ts
│   │   └── products.ts
│   ├── middleware/
│   │   ├── auth.ts
│   │   └── logger.ts
│   ├── services/
│   │   └── database.ts
│   └── types/
│       └── index.ts
├── package.json
└── tsconfig.json

أنشئ المجلدات والملفات:

mkdir -p src/{routes,middleware,services,types}
touch src/index.ts
touch src/routes/{index,users,products}.ts
touch src/middleware/{auth,logger}.ts
touch src/services/database.ts
touch src/types/index.ts

الخطوة 5: تعريف الأنواع

في src/types/index.ts:

export interface User {
  id: string
  name: string
  email: string
  createdAt: Date
}
 
export interface Product {
  id: string
  name: string
  price: number
  description: string
  stock: number
}
 
export interface ApiResponse<T> {
  success: boolean
  data?: T
  error?: string
  meta?: {
    page?: number
    total?: number
  }
}

الخطوة 6: إنشاء Middleware للتسجيل

في src/middleware/logger.ts:

import { MiddlewareHandler } from 'hono'
 
export const logger: MiddlewareHandler = async (c, next) => {
  const start = Date.now()
  const method = c.req.method
  const path = c.req.path
 
  console.log(`→ ${method} ${path}`)
 
  await next()
 
  const duration = Date.now() - start
  const status = c.res.status
 
  console.log(`← ${method} ${path} ${status} (${duration}ms)`)
}

الخطوة 7: بناء Middleware للمصادقة

في src/middleware/auth.ts:

import { MiddlewareHandler } from 'hono'
import { HTTPException } from 'hono/http-exception'
 
// مصادقة بسيطة بمفتاح API
export const authMiddleware: MiddlewareHandler = async (c, next) => {
  const apiKey = c.req.header('X-API-Key')
 
  if (!apiKey) {
    throw new HTTPException(401, {
      message: 'مفتاح API مطلوب'
    })
  }
 
  // في الإنتاج، تحقق من قاعدة البيانات
  const validKeys = ['demo-key-123', 'test-key-456']
 
  if (!validKeys.includes(apiKey)) {
    throw new HTTPException(403, {
      message: 'مفتاح API غير صالح'
    })
  }
 
  // إرفاق معلومات المستخدم بالسياق
  c.set('apiKey', apiKey)
 
  await next()
}
 
// مثال على مصادقة JWT
export const jwtAuth: MiddlewareHandler = async (c, next) => {
  const authHeader = c.req.header('Authorization')
 
  if (!authHeader?.startsWith('Bearer ')) {
    throw new HTTPException(401, {
      message: 'رمز Bearer مطلوب'
    })
  }
 
  const token = authHeader.slice(7)
 
  try {
    // في الإنتاج، تحقق من JWT بشكل صحيح
    // const payload = await verifyJwt(token)
    // c.set('user', payload)
    await next()
  } catch {
    throw new HTTPException(401, {
      message: 'رمز غير صالح'
    })
  }
}

الخطوة 8: إعداد خدمة قاعدة البيانات

نستخدم SQLite المدمج في Bun. في src/services/database.ts:

import { Database } from 'bun:sqlite'
import type { User, Product } from '../types'
 
// تهيئة قاعدة البيانات
const db = new Database('app.db')
 
// إنشاء الجداول
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`)
 
db.run(`
  CREATE TABLE IF NOT EXISTS products (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    price REAL NOT NULL,
    description TEXT,
    stock INTEGER DEFAULT 0
  )
`)
 
// عمليات المستخدمين
export const userService = {
  findAll(): User[] {
    return db.query('SELECT * FROM users').all() as User[]
  },
 
  findById(id: string): User | null {
    return db.query('SELECT * FROM users WHERE id = ?').get(id) as User | null
  },
 
  create(user: Omit<User, 'createdAt'>): User {
    const stmt = db.prepare(
      'INSERT INTO users (id, name, email) VALUES (?, ?, ?)'
    )
    stmt.run(user.id, user.name, user.email)
    return this.findById(user.id)!
  },
 
  update(id: string, data: Partial<User>): User | null {
    const fields = Object.keys(data)
      .map(k => `${k} = ?`)
      .join(', ')
    const values = Object.values(data)
 
    db.prepare(`UPDATE users SET ${fields} WHERE id = ?`).run(...values, id)
    return this.findById(id)
  },
 
  delete(id: string): boolean {
    const result = db.prepare('DELETE FROM users WHERE id = ?').run(id)
    return result.changes > 0
  }
}
 
// عمليات المنتجات
export const productService = {
  findAll(): Product[] {
    return db.query('SELECT * FROM products').all() as Product[]
  },
 
  findById(id: string): Product | null {
    return db.query('SELECT * FROM products WHERE id = ?').get(id) as Product | null
  },
 
  create(product: Product): Product {
    const stmt = db.prepare(
      'INSERT INTO products (id, name, price, description, stock) VALUES (?, ?, ?, ?, ?)'
    )
    stmt.run(product.id, product.name, product.price, product.description, product.stock)
    return this.findById(product.id)!
  },
 
  update(id: string, data: Partial<Product>): Product | null {
    const fields = Object.keys(data)
      .map(k => `${k} = ?`)
      .join(', ')
    const values = Object.values(data)
 
    db.prepare(`UPDATE products SET ${fields} WHERE id = ?`).run(...values, id)
    return this.findById(id)
  },
 
  delete(id: string): boolean {
    const result = db.prepare('DELETE FROM products WHERE id = ?').run(id)
    return result.changes > 0
  }
}
 
export { db }

الخطوة 9: إنشاء مسارات المستخدمين

في src/routes/users.ts:

import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { userService } from '../services/database'
import type { ApiResponse, User } from '../types'
 
const users = new Hono()
 
// مخططات التحقق
const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email()
})
 
const updateUserSchema = z.object({
  name: z.string().min(2).max(100).optional(),
  email: z.string().email().optional()
})
 
// GET /users - قائمة المستخدمين
users.get('/', (c) => {
  const allUsers = userService.findAll()
 
  const response: ApiResponse<User[]> = {
    success: true,
    data: allUsers,
    meta: { total: allUsers.length }
  }
 
  return c.json(response)
})
 
// GET /users/:id - مستخدم واحد
users.get('/:id', (c) => {
  const id = c.req.param('id')
  const user = userService.findById(id)
 
  if (!user) {
    return c.json<ApiResponse<null>>({
      success: false,
      error: 'المستخدم غير موجود'
    }, 404)
  }
 
  return c.json<ApiResponse<User>>({
    success: true,
    data: user
  })
})
 
// POST /users - إنشاء مستخدم
users.post(
  '/',
  zValidator('json', createUserSchema),
  (c) => {
    const body = c.req.valid('json')
    const id = crypto.randomUUID()
 
    try {
      const user = userService.create({ id, ...body })
 
      return c.json<ApiResponse<User>>({
        success: true,
        data: user
      }, 201)
    } catch (error) {
      return c.json<ApiResponse<null>>({
        success: false,
        error: 'البريد الإلكتروني موجود مسبقاً'
      }, 409)
    }
  }
)
 
// PUT /users/:id - تحديث مستخدم
users.put(
  '/:id',
  zValidator('json', updateUserSchema),
  (c) => {
    const id = c.req.param('id')
    const body = c.req.valid('json')
 
    const user = userService.update(id, body)
 
    if (!user) {
      return c.json<ApiResponse<null>>({
        success: false,
        error: 'المستخدم غير موجود'
      }, 404)
    }
 
    return c.json<ApiResponse<User>>({
      success: true,
      data: user
    })
  }
)
 
// DELETE /users/:id - حذف مستخدم
users.delete('/:id', (c) => {
  const id = c.req.param('id')
  const deleted = userService.delete(id)
 
  if (!deleted) {
    return c.json<ApiResponse<null>>({
      success: false,
      error: 'المستخدم غير موجود'
    }, 404)
  }
 
  return c.json<ApiResponse<null>>({
    success: true
  }, 204)
})
 
export default users

لا تنسَ تثبيت Zod للتحقق:

bun add zod @hono/zod-validator

الخطوة 10: إنشاء مسارات المنتجات

في src/routes/products.ts:

import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { productService } from '../services/database'
import type { ApiResponse, Product } from '../types'
 
const products = new Hono()
 
const productSchema = z.object({
  name: z.string().min(1).max(200),
  price: z.number().positive(),
  description: z.string().optional(),
  stock: z.number().int().min(0).default(0)
})
 
// GET /products
products.get('/', (c) => {
  const query = c.req.query('search')
  let allProducts = productService.findAll()
 
  if (query) {
    allProducts = allProducts.filter(p =>
      p.name.toLowerCase().includes(query.toLowerCase())
    )
  }
 
  return c.json<ApiResponse<Product[]>>({
    success: true,
    data: allProducts,
    meta: { total: allProducts.length }
  })
})
 
// GET /products/:id
products.get('/:id', (c) => {
  const product = productService.findById(c.req.param('id'))
 
  if (!product) {
    return c.json<ApiResponse<null>>({
      success: false,
      error: 'المنتج غير موجود'
    }, 404)
  }
 
  return c.json<ApiResponse<Product>>({
    success: true,
    data: product
  })
})
 
// POST /products
products.post(
  '/',
  zValidator('json', productSchema),
  (c) => {
    const body = c.req.valid('json')
    const id = crypto.randomUUID()
 
    const product = productService.create({
      id,
      name: body.name,
      price: body.price,
      description: body.description || '',
      stock: body.stock
    })
 
    return c.json<ApiResponse<Product>>({
      success: true,
      data: product
    }, 201)
  }
)
 
// PUT /products/:id
products.put(
  '/:id',
  zValidator('json', productSchema.partial()),
  (c) => {
    const id = c.req.param('id')
    const body = c.req.valid('json')
 
    const product = productService.update(id, body)
 
    if (!product) {
      return c.json<ApiResponse<null>>({
        success: false,
        error: 'المنتج غير موجود'
      }, 404)
    }
 
    return c.json<ApiResponse<Product>>({
      success: true,
      data: product
    })
  }
)
 
// DELETE /products/:id
products.delete('/:id', (c) => {
  const deleted = productService.delete(c.req.param('id'))
 
  if (!deleted) {
    return c.json<ApiResponse<null>>({
      success: false,
      error: 'المنتج غير موجود'
    }, 404)
  }
 
  return c.json({ success: true }, 204)
})
 
export default products

الخطوة 11: دمج المسارات

في src/routes/index.ts:

import { Hono } from 'hono'
import users from './users'
import products from './products'
 
const routes = new Hono()
 
routes.route('/users', users)
routes.route('/products', products)
 
export default routes

الخطوة 12: نقطة الدخول الرئيسية

حدّث src/index.ts:

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { prettyJSON } from 'hono/pretty-json'
import { HTTPException } from 'hono/http-exception'
 
import { logger } from './middleware/logger'
import { authMiddleware } from './middleware/auth'
import routes from './routes'
 
const app = new Hono()
 
// Middleware عام
app.use('*', logger)
app.use('*', cors({
  origin: ['http://localhost:3000', 'https://yourdomain.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization', 'X-API-Key']
}))
app.use('*', prettyJSON())
 
// المسارات العامة
app.get('/', (c) => {
  return c.json({
    name: 'Hono REST API',
    version: '1.0.0',
    docs: '/docs',
    health: '/health'
  })
})
 
app.get('/health', (c) => c.json({ status: 'ok' }))
 
// مسارات API المحمية
app.use('/api/*', authMiddleware)
app.route('/api', routes)
 
// معالج الأخطاء العام
app.onError((err, c) => {
  console.error('خطأ:', err)
 
  if (err instanceof HTTPException) {
    return c.json({
      success: false,
      error: err.message
    }, err.status)
  }
 
  return c.json({
    success: false,
    error: 'خطأ في الخادم الداخلي'
  }, 500)
})
 
// معالج 404
app.notFound((c) => {
  return c.json({
    success: false,
    error: 'المسار غير موجود'
  }, 404)
})
 
// تصدير لـ Bun
export default {
  port: process.env.PORT || 3000,
  fetch: app.fetch
}
 
console.log('🚀 الخادم يعمل على http://localhost:3000')

الخطوة 13: اختبار الـ API

شغّل الخادم:

bun run src/index.ts

اختبر باستخدام curl:

# فحص الصحة
curl http://localhost:3000/health
 
# إنشاء مستخدم (مع مفتاح API)
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -H "X-API-Key: demo-key-123" \
  -d '{"name": "أحمد محمد", "email": "ahmed@example.com"}'
 
# قائمة المستخدمين
curl http://localhost:3000/api/users \
  -H "X-API-Key: demo-key-123"
 
# إنشاء منتج
curl -X POST http://localhost:3000/api/products \
  -H "Content-Type: application/json" \
  -H "X-API-Key: demo-key-123" \
  -d '{"name": "حاسوب محمول", "price": 999.99, "stock": 50}'

لا تقم أبداً بحفظ مفاتيح API في نظام التحكم بالإصدار. استخدم متغيرات البيئة للبيانات الحساسة في الإنتاج.

الخطوة 14: إضافة توثيق OpenAPI

يدعم Hono توثيق OpenAPI/Swagger. ثبّت الحزمة:

bun add @hono/swagger-ui

أضف نقطة التوثيق إلى src/index.ts:

import { swaggerUI } from '@hono/swagger-ui'
 
// أضف بعد المسارات الأخرى
app.get('/docs', swaggerUI({ url: '/openapi.json' }))
 
app.get('/openapi.json', (c) => {
  return c.json({
    openapi: '3.0.0',
    info: {
      title: 'Hono REST API',
      version: '1.0.0',
      description: 'واجهة REST API حديثة مبنية بـ Hono و Bun'
    },
    servers: [
      { url: 'http://localhost:3000', description: 'التطوير' }
    ],
    paths: {
      '/api/users': {
        get: {
          summary: 'قائمة جميع المستخدمين',
          security: [{ apiKey: [] }],
          responses: {
            '200': { description: 'قائمة المستخدمين' }
          }
        }
      }
      // أضف المزيد من المسارات...
    },
    components: {
      securitySchemes: {
        apiKey: {
          type: 'apiKey',
          in: 'header',
          name: 'X-API-Key'
        }
      }
    }
  })
})

الخطوة 15: النشر

الخيار 1: Docker

أنشئ Dockerfile:

FROM oven/bun:1.1-alpine
 
WORKDIR /app
 
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
 
COPY . .
 
ENV NODE_ENV=production
EXPOSE 3000
 
CMD ["bun", "run", "src/index.ts"]

ابنِ وشغّل:

docker build -t hono-api .
docker run -p 3000:3000 hono-api

الخيار 2: Fly.io

أنشئ fly.toml:

app = "your-hono-api"
primary_region = "fra"
 
[build]
  builder = "oven/bun:1.1"
 
[http_service]
  internal_port = 3000
  force_https = true
 
[env]
  NODE_ENV = "production"

انشر:

fly launch
fly deploy

الخيار 3: Cloudflare Workers

يعمل Hono بسلاسة مع Workers. حدّث التصدير:

export default app

انشر مع Wrangler:

bunx wrangler deploy

مقارنة الأداء

إليك لماذا يتفوق Hono + Bun على Express + Node:

المقياسExpress + NodeHono + Bun
طلبات/ثانية~15,000~90,000
وقت البدء~300ms~25ms
استخدام الذاكرة~50MB~20MB
حجم الحزمةثقيل~14KB

ملخص

لقد بنيت REST API كاملاً مع:

  • ✅ إطار Hono للتوجيه والـ Middleware
  • ✅ بيئة Bun لأقصى أداء
  • ✅ TypeScript لأمان الأنواع
  • ✅ قاعدة بيانات SQLite مع عمليات CRUD
  • ✅ التحقق من المدخلات مع Zod
  • ✅ Middleware للمصادقة
  • ✅ CORS ومعالجة الأخطاء
  • ✅ توثيق OpenAPI
  • ✅ خيارات النشر

الخطوات التالية

  • أضف تحديد معدل الطلبات مع hono/rate-limit
  • طبّق مصادقة JWT كاملة
  • أضف ترحيل قاعدة البيانات
  • أعد خط أنابيب CI/CD
  • أضف اختبارات الوحدات والتكامل

يمثل الجمع بين Hono و Bun مستقبل تطوير الـ Backend بـ JavaScript - سريع وآمن للأنواع وصديق للمطور. ابدأ البناء!

المصادر


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على استكشاف Transformers.js.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة