بناء ونشر واجهة برمجية بدون خوادم باستخدام Cloudflare Workers و Hono و D1

AI Bot
بواسطة AI Bot ·

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

انشر واجهاتك البرمجية على حافة الشبكة، عالمياً، في ثوانٍ. تعمل Cloudflare Workers في أكثر من 300 مركز بيانات حول العالم بدون أي تأخير في بدء التشغيل. عند دمجها مع التوجيه فائق السرعة من Hono وقاعدة بيانات D1 السحابية، تحصل على واجهة برمجية متكاملة دون إدارة أي خادم.

ما الذي ستبنيه؟

في هذا الدليل العملي، ستُنشئ واجهة برمجية لإدارة المهام متكاملة تدعم عمليات CRUD، مدعومة بقاعدة بيانات D1 (SQLite سحابية)، مبنية بإطار Hono، ومنشورة عالمياً على Cloudflare Workers. في النهاية، سيكون لديك API جاهز للإنتاج يعمل على حافة الشبكة.

مميزات الواجهة البرمجية النهائية:

  • نقاط وصول RESTful للمهام (إنشاء، قراءة، تحديث، حذف)
  • التحقق من المدخلات ومعالجة الأخطاء
  • قاعدة بيانات SQLite مع نظام ترحيل (migrations)
  • دعم CORS للاستهلاك من الواجهات الأمامية
  • نشر عالمي بزمن استجابة شبه معدوم

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

قبل البدء، تأكد من توفر التالي:

  • Node.js 18+ مثبّت على جهازك (حمّله من هنا)
  • حساب Cloudflare — الباقة المجانية كافية (سجّل هنا)
  • أداة Wrangler CLI — أداة تطوير Cloudflare (سنثبّتها معاً)
  • معرفة أساسية بـ TypeScript و واجهات REST البرمجية
  • محرر أكواد (يُفضل VS Code)

الباقة المجانية من Cloudflare Workers تشمل 100,000 طلب يومياً، وD1 يوفر 5 ملايين قراءة صف يومياً — أكثر من كافٍ لمعظم المشاريع والنماذج الأولية.


الخطوة 1: تثبيت Wrangler والمصادقة

Wrangler هي أداة سطر الأوامر لتطوير ونشر Cloudflare Workers. ثبّتها عالمياً:

npm install -g wrangler

ثم سجّل الدخول بحسابك على Cloudflare:

wrangler login

سيُفتح المتصفح تلقائياً. وافق على التفويض، ثم تحقق من الاتصال:

wrangler whoami

يجب أن ترى اسم حسابك ومعرّفه.


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

أنشئ مشروع Hono جديد مهيّأ لـ Cloudflare Workers:

npm create hono@latest task-api

عند ظهور الخيارات:

  • أي قالب؟cloudflare-workers
  • مدير الحزم؟npm (أو ما تفضّله)

انتقل إلى مجلد المشروع:

cd task-api
npm install

هيكل المشروع يبدو هكذا:

task-api/
├── src/
│   └── index.ts        # نقطة الدخول الرئيسية
├── wrangler.toml       # إعدادات Cloudflare
├── package.json
└── tsconfig.json

الخطوة 3: إنشاء قاعدة بيانات D1

D1 هي قاعدة بيانات SQLite السحابية من Cloudflare. أنشئ واحدة لمشروعك:

wrangler d1 create task-db

سيتضمن الناتج معرّف قاعدة البيانات. انسخه — ستحتاجه في الإعدادات:

✅ Successfully created DB 'task-db'

[[d1_databases]]
binding = "DB"
database_name = "task-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

افتح ملف wrangler.toml وأضف ربط D1:

name = "task-api"
main = "src/index.ts"
compatibility_date = "2026-02-25"
 
[[d1_databases]]
binding = "DB"
database_name = "task-db"
database_id = "ضع_معرّف_قاعدة_البيانات_هنا"

استبدل ضع_معرّف_قاعدة_البيانات_هنا بالمعرّف الفعلي من الناتج السابق.


الخطوة 4: تعريف مخطط قاعدة البيانات

أنشئ ملف schema.sql في جذر المشروع:

-- schema.sql
DROP TABLE IF EXISTS tasks;
 
CREATE TABLE tasks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  description TEXT DEFAULT '',
  status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed')),
  priority INTEGER DEFAULT 0 CHECK(priority BETWEEN 0 AND 3),
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);
 
-- بيانات أولية للتجربة
INSERT INTO tasks (title, description, status, priority) VALUES
  ('إعداد خط CI/CD', 'تهيئة GitHub Actions للنشر التلقائي', 'pending', 2),
  ('كتابة توثيق الواجهة البرمجية', 'إنشاء مواصفات OpenAPI لواجهة إدارة المهام', 'in_progress', 1),
  ('تصميم مخطط قاعدة البيانات', 'إنهاء مخطط ERD للمشروع', 'completed', 3);

طبّق المخطط على قاعدة البيانات المحلية (للتطوير):

wrangler d1 execute task-db --local --file=./schema.sql

وعلى قاعدة البيانات عن بُعد (الإنتاج):

wrangler d1 execute task-db --remote --file=./schema.sql

العلامة --remote تُعدّل قاعدة بيانات الإنتاج مباشرة. في المشاريع الحقيقية، استخدم نظام الترحيل D1 migrations (wrangler d1 migrations) لتغييرات آمنة ومُتحكَّم بها.


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

أنشئ ملف src/types.ts لتعريف نماذج البيانات والربط:

// src/types.ts
 
export interface Env {
  DB: D1Database;
}
 
export interface Task {
  id: number;
  title: string;
  description: string;
  status: 'pending' | 'in_progress' | 'completed';
  priority: number;
  created_at: string;
  updated_at: string;
}
 
export interface CreateTaskInput {
  title: string;
  description?: string;
  status?: Task['status'];
  priority?: number;
}
 
export interface UpdateTaskInput {
  title?: string;
  description?: string;
  status?: Task['status'];
  priority?: number;
}

الخطوة 6: بناء مسارات الواجهة البرمجية

الآن ننتقل لجوهر التطبيق. استبدل محتوى src/index.ts بتطبيق Hono الكامل:

// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import type { Env, Task, CreateTaskInput, UpdateTaskInput } from './types';
 
const app = new Hono<{ Bindings: Env }>();
 
// ─── الوسطاء (Middleware) ────────────────────────────────────
app.use('/*', cors());
 
// ─── فحص الحالة ──────────────────────────────────────────────
app.get('/', (c) => {
  return c.json({
    status: 'ok',
    service: 'Task Management API',
    version: '1.0.0',
    timestamp: new Date().toISOString(),
  });
});
 
// ─── GET /tasks ──────────────────────────────────────────────
// عرض جميع المهام مع إمكانية التصفية
app.get('/tasks', async (c) => {
  const status = c.req.query('status');
  const sortBy = c.req.query('sort') || 'created_at';
  const order = c.req.query('order') || 'desc';
  const limit = Math.min(parseInt(c.req.query('limit') || '50'), 100);
  const offset = parseInt(c.req.query('offset') || '0');
 
  let query = 'SELECT * FROM tasks';
  const params: string[] = [];
 
  if (status) {
    query += ' WHERE status = ?';
    params.push(status);
  }
 
  const allowedSorts = ['created_at', 'updated_at', 'priority', 'title'];
  const safeSort = allowedSorts.includes(sortBy) ? sortBy : 'created_at';
  const safeOrder = order.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
 
  query += ` ORDER BY ${safeSort} ${safeOrder} LIMIT ? OFFSET ?`;
  params.push(limit.toString(), offset.toString());
 
  try {
    const result = await c.env.DB.prepare(query)
      .bind(...params)
      .all<Task>();
 
    return c.json({
      tasks: result.results,
      meta: {
        total: result.results.length,
        limit,
        offset,
      },
    });
  } catch (error) {
    return c.json({ error: 'فشل في جلب المهام' }, 500);
  }
});
 
// ─── GET /tasks/:id ─────────────────────────────────────────
// جلب مهمة واحدة بالمعرّف
app.get('/tasks/:id', async (c) => {
  const id = c.req.param('id');
 
  try {
    const task = await c.env.DB.prepare(
      'SELECT * FROM tasks WHERE id = ?'
    )
      .bind(id)
      .first<Task>();
 
    if (!task) {
      return c.json({ error: 'المهمة غير موجودة' }, 404);
    }
 
    return c.json({ task });
  } catch (error) {
    return c.json({ error: 'فشل في جلب المهمة' }, 500);
  }
});
 
// ─── POST /tasks ─────────────────────────────────────────────
// إنشاء مهمة جديدة
app.post('/tasks', async (c) => {
  let body: CreateTaskInput;
 
  try {
    body = await c.req.json<CreateTaskInput>();
  } catch {
    return c.json({ error: 'جسم الطلب JSON غير صالح' }, 400);
  }
 
  if (!body.title || body.title.trim().length === 0) {
    return c.json({ error: 'العنوان مطلوب' }, 400);
  }
 
  if (body.title.length > 255) {
    return c.json({ error: 'العنوان يجب ألا يتجاوز 255 حرفاً' }, 400);
  }
 
  const validStatuses = ['pending', 'in_progress', 'completed'];
  if (body.status && !validStatuses.includes(body.status)) {
    return c.json({ error: `الحالة يجب أن تكون إحدى: ${validStatuses.join(', ')}` }, 400);
  }
 
  if (body.priority !== undefined && (body.priority < 0 || body.priority > 3)) {
    return c.json({ error: 'الأولوية يجب أن تكون بين 0 و 3' }, 400);
  }
 
  try {
    const result = await c.env.DB.prepare(
      `INSERT INTO tasks (title, description, status, priority)
       VALUES (?, ?, ?, ?)
       RETURNING *`
    )
      .bind(
        body.title.trim(),
        body.description || '',
        body.status || 'pending',
        body.priority ?? 0
      )
      .first<Task>();
 
    return c.json({ task: result }, 201);
  } catch (error) {
    return c.json({ error: 'فشل في إنشاء المهمة' }, 500);
  }
});
 
// ─── PUT /tasks/:id ─────────────────────────────────────────
// تحديث مهمة موجودة
app.put('/tasks/:id', async (c) => {
  const id = c.req.param('id');
  let body: UpdateTaskInput;
 
  try {
    body = await c.req.json<UpdateTaskInput>();
  } catch {
    return c.json({ error: 'جسم الطلب JSON غير صالح' }, 400);
  }
 
  const existing = await c.env.DB.prepare(
    'SELECT * FROM tasks WHERE id = ?'
  )
    .bind(id)
    .first<Task>();
 
  if (!existing) {
    return c.json({ error: 'المهمة غير موجودة' }, 404);
  }
 
  const updates: string[] = [];
  const values: (string | number)[] = [];
 
  if (body.title !== undefined) {
    if (body.title.trim().length === 0) {
      return c.json({ error: 'العنوان لا يمكن أن يكون فارغاً' }, 400);
    }
    updates.push('title = ?');
    values.push(body.title.trim());
  }
 
  if (body.description !== undefined) {
    updates.push('description = ?');
    values.push(body.description);
  }
 
  if (body.status !== undefined) {
    const validStatuses = ['pending', 'in_progress', 'completed'];
    if (!validStatuses.includes(body.status)) {
      return c.json({ error: `الحالة يجب أن تكون إحدى: ${validStatuses.join(', ')}` }, 400);
    }
    updates.push('status = ?');
    values.push(body.status);
  }
 
  if (body.priority !== undefined) {
    if (body.priority < 0 || body.priority > 3) {
      return c.json({ error: 'الأولوية يجب أن تكون بين 0 و 3' }, 400);
    }
    updates.push('priority = ?');
    values.push(body.priority);
  }
 
  if (updates.length === 0) {
    return c.json({ error: 'لا توجد حقول للتحديث' }, 400);
  }
 
  updates.push("updated_at = datetime('now')");
  values.push(id);
 
  try {
    const result = await c.env.DB.prepare(
      `UPDATE tasks SET ${updates.join(', ')} WHERE id = ? RETURNING *`
    )
      .bind(...values)
      .first<Task>();
 
    return c.json({ task: result });
  } catch (error) {
    return c.json({ error: 'فشل في تحديث المهمة' }, 500);
  }
});
 
// ─── DELETE /tasks/:id ───────────────────────────────────────
// حذف مهمة
app.delete('/tasks/:id', async (c) => {
  const id = c.req.param('id');
 
  try {
    const existing = await c.env.DB.prepare(
      'SELECT id FROM tasks WHERE id = ?'
    )
      .bind(id)
      .first();
 
    if (!existing) {
      return c.json({ error: 'المهمة غير موجودة' }, 404);
    }
 
    await c.env.DB.prepare('DELETE FROM tasks WHERE id = ?')
      .bind(id)
      .run();
 
    return c.json({ message: 'تم حذف المهمة بنجاح' });
  } catch (error) {
    return c.json({ error: 'فشل في حذف المهمة' }, 500);
  }
});
 
// ─── معالج 404 ───────────────────────────────────────────────
app.notFound((c) => {
  return c.json({ error: 'غير موجود' }, 404);
});
 
// ─── معالج الأخطاء ──────────────────────────────────────────
app.onError((err, c) => {
  console.error('خطأ غير متوقع:', err);
  return c.json({ error: 'خطأ داخلي في الخادم' }, 500);
});
 
export default app;

الخطوة 7: الاختبار المحلي

يوفر Wrangler خادم تطوير محلي يحاكي بيئة Workers، بما في ذلك D1:

wrangler dev

واجهتك البرمجية تعمل الآن على http://localhost:8787. اختبرها باستخدام curl:

# فحص الحالة
curl http://localhost:8787/
 
# عرض جميع المهام
curl http://localhost:8787/tasks
 
# إنشاء مهمة جديدة
curl -X POST http://localhost:8787/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "تعلّم Cloudflare Workers", "description": "إكمال هذا الدليل", "priority": 2}'
 
# جلب مهمة محددة
curl http://localhost:8787/tasks/1
 
# تحديث مهمة
curl -X PUT http://localhost:8787/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}'
 
# حذف مهمة
curl -X DELETE http://localhost:8787/tasks/4
 
# التصفية حسب الحالة
curl "http://localhost:8787/tasks?status=pending&sort=priority&order=desc"

ستحصل على استجابات JSON لكل عملية. قاعدة البيانات المحلية محفوظة في .wrangler/state/.

إعادة التحميل التلقائي: يراقب Wrangler ملفاتك ويُعيد التحميل تلقائياً عند الحفظ. لا حاجة لإعادة تشغيل خادم التطوير.


الخطوة 8: إضافة وسيط التحقق من الطلبات

لواجهة برمجية أكثر متانة، لنضف وسيطاً قابلاً لإعادة الاستخدام. أنشئ src/middleware.ts:

// src/middleware.ts
import { Context, Next } from 'hono';
 
export function validateJson() {
  return async (c: Context, next: Next) => {
    if (['POST', 'PUT', 'PATCH'].includes(c.req.method)) {
      const contentType = c.req.header('content-type');
      if (!contentType?.includes('application/json')) {
        return c.json(
          { error: 'يجب أن يكون Content-Type هو application/json' },
          415
        );
      }
    }
    await next();
  };
}
 
export function requestLogger() {
  return async (c: Context, next: Next) => {
    const start = Date.now();
    await next();
    const duration = Date.now() - start;
    console.log(
      `${c.req.method} ${c.req.path} → ${c.res.status} (${duration}ms)`
    );
  };
}
 
export function rateLimit(maxRequests: number, windowMs: number) {
  const requests = new Map<string, { count: number; resetAt: number }>();
 
  return async (c: Context, next: Next) => {
    const ip = c.req.header('cf-connecting-ip') || 'unknown';
    const now = Date.now();
    const record = requests.get(ip);
 
    if (!record || now > record.resetAt) {
      requests.set(ip, { count: 1, resetAt: now + windowMs });
    } else if (record.count >= maxRequests) {
      return c.json({ error: 'عدد كبير جداً من الطلبات' }, 429);
    } else {
      record.count++;
    }
 
    await next();
  };
}

ثم أضف الوسطاء إلى index.ts:

import { validateJson, requestLogger } from './middleware';
 
// أضف بعد cors()
app.use('/*', requestLogger());
app.use('/tasks/*', validateJson());

الخطوة 9: إضافة ترحيل D1 (أفضل ممارسة للإنتاج)

بدلاً من تنفيذ SQL مباشرةً، استخدم نظام الترحيل من Wrangler في الإنتاج:

# إنشاء مجلد الترحيل
wrangler d1 migrations create task-db init

هذا ينشئ ملفاً في migrations/. الصق SQL المخطط فيه. ثم طبّق:

# تطبيق محلياً
wrangler d1 migrations apply task-db --local
 
# تطبيق على الإنتاج
wrangler d1 migrations apply task-db --remote

التغييرات المستقبلية في المخطط تصبح ملفات ترحيل جديدة، مما يمنحك تحكماً بالإصدارات في مخطط قاعدة بياناتك.


الخطوة 10: النشر على الإنتاج

النشر بأمر واحد:

wrangler deploy

الناتج:

⛅️ wrangler 3.x.x
Uploaded task-api (1.42 sec)
Published task-api (0.35 sec)
  https://task-api.YOUR_SUBDOMAIN.workers.dev

واجهتك البرمجية الآن متاحة على شبكة Cloudflare العالمية! اختبرها:

curl https://task-api.YOUR_SUBDOMAIN.workers.dev/tasks

لا تأخير في بدء التشغيل. على عكس AWS Lambda أو Google Cloud Functions، تبدأ Cloudflare Workers في أقل من 5 مللي ثانية. واجهتك البرمجية تستجيب فوراً، من أي مكان في العالم.


الخطوة 11: إضافة نطاق مخصص (اختياري)

إذا أردت واجهتك البرمجية على نطاق مخصص، أضف مساراً في wrangler.toml:

routes = [
  { pattern = "api.yourdomain.com/*", zone_name = "yourdomain.com" }
]

تأكد من إضافة النطاق إلى حسابك على Cloudflare. ثم أعد النشر:

wrangler deploy

واجهتك البرمجية الآن متاحة على https://api.yourdomain.com/tasks.


الخطوة 12: المراقبة وتصحيح الأخطاء

توفر Cloudflare سجلات حية لـ Workers:

# بث السجلات الحية من الإنتاج
wrangler tail

سترى كل طلب وحالة استجابة وأي console.log في الوقت الفعلي. للمراقبة المتقدمة، تحقق من لوحة Workers Analytics في واجهة Cloudflare.

لتصحيح الأخطاء محلياً مع نقاط التوقف:

wrangler dev --inspect

ثم اربط Chrome DevTools بالعنوان المعروض.


هيكل المشروع النهائي

task-api/
├── src/
│   ├── index.ts          # تطبيق Hono الرئيسي مع المسارات
│   ├── types.ts          # واجهات TypeScript
│   └── middleware.ts      # الوسطاء المخصصون
├── migrations/
│   └── 0001_init.sql     # ترحيل D1
├── schema.sql            # المخطط الأولي (مرجع)
├── wrangler.toml         # إعدادات Cloudflare
├── package.json
└── tsconfig.json

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

لماذا تختار هذه الحزمة التقنية؟ إليك المقارنة:

المعيارCloudflare Workers + D1AWS Lambda + RDSVercel Serverless
تأخير البدء< 5 مللي ثانية100-500 مللي ثانية50-250 مللي ثانية
التوزيع العالمي300+ موقعحسب المنطقةحسب المنطقة
زمن استجابة قاعدة البياناتD1 مشترك الموقعيعتمد على VPCقاعدة بيانات خارجية
الباقة المجانية100 ألف طلب/يوممليون طلب/شهر100 ألف طلب/شهر
السعر (المدفوع)$0.30/مليون طلب$0.20/مليون + حوسبة$0.60/مليون طلب

نصائح وأفضل الممارسات

اجعل Workers خفيفاً. حد الحجم المضغوط 1 ميجابايت يشجع على بناء خدمات صغيرة ومركّزة. إذا كبرت واجهتك البرمجية، قسّمها إلى عدة Workers.

  1. استخدم تجميع D1 للعمليات المجمّعة — db.batch([stmt1, stmt2]) ينفذ عدة عبارات في رحلة واحدة.

  2. فعّل Smart Placement في wrangler.toml للسماح لـ Cloudflare بوضع Worker تلقائياً بالقرب من قاعدة D1:

    [placement]
    mode = "smart"
  3. استخدم bindings للأسرار بدلاً من كتابة مفاتيح API في الكود:

    wrangler secret put API_KEY

    الوصول عبر c.env.API_KEY في كودك.

  4. استفد من وسطاء Hono المدمجين — يتضمن مصادقة JWT، Bearer، Basic، ETag، والمزيد:

    import { bearerAuth } from 'hono/bearer-auth';
    app.use('/admin/*', bearerAuth({ token: 'secret' }));
  5. استخدم ctx.executionCtx.waitUntil() للمهام غير الحظرية كالتسجيل أو التحليلات التي لا يجب أن تؤخر الاستجابة.


ما التالي؟

لديك الآن واجهة برمجية جاهزة للإنتاج تعمل عالمياً. إليك أفكاراً للتوسع:

  • إضافة المصادقة باستخدام Cloudflare Access أو رموز JWT
  • تطبيق ترقيم الصفحات بنظام المؤشر (cursor-based) للبيانات الكبيرة
  • إضافة البحث النصي الكامل باستخدام إضافة FTS5 في D1
  • إنشاء واجهة أمامية بـ React أو Next.js تستهلك واجهتك البرمجية
  • إعداد CI/CD باستخدام GitHub Actions و wrangler deploy
  • إضافة التخزين المؤقت باستخدام Cache API أو Cloudflare KV للنقاط كثيفة القراءة

الملخص

في هذا الدليل، بنيت واجهة برمجية REST متكاملة بدون خوادم باستخدام ثلاث تقنيات قوية:

  • Cloudflare Workers — حوسبة بدون خوادم تعمل على حافة الشبكة بدون أي تأخير في البدء
  • Hono — إطار ويب فائق السرعة مصمم لبيئات الحافة
  • D1 — قاعدة بيانات SQLite السحابية من Cloudflare مع نسخ عالمي

أعددت المشروع، عرّفت مخطط قاعدة البيانات، بنيت مسارات CRUD مع التحقق ومعالجة الأخطاء، اختبرت محلياً، ونشرت عالمياً — كل ذلك بدون تجهيز أو إدارة أي خادم. الحزمة بأكملها تعمل على الباقة المجانية من Cloudflare، مما يجعلها خياراً ممتازاً للمشاريع الجانبية والنماذج الأولية وواجهات الإنتاج على حد سواء.

الحوسبة السحابية على الحافة لم تعد المستقبل — إنها الحاضر. ابدأ البناء! 🚀


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على 13 أساسيات Laravel 11: التحقق من الصحة.

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

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

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

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

بناء وكلاء الذكاء الاصطناعي من الصفر باستخدام TypeScript: إتقان نمط ReAct مع Vercel AI SDK

تعلّم كيفية بناء وكلاء الذكاء الاصطناعي من الأساس باستخدام TypeScript. يغطي هذا الدليل التعليمي نمط ReAct، واستدعاء الأدوات، والاستدلال متعدد الخطوات، وحلقات الوكلاء الجاهزة للإنتاج مع Vercel AI SDK.

35 د قراءة·