---
title: "دليل Cloudflare Workers: من أول طلب إلى تطبيق يعمل"
description: "دليل عملي لفهم Cloudflare Workers وبناء واجهة برمجية باستخدام Hono: من تقدير مدة قراءة النصوص إلى النشر وربط الخدمات ومتابعة الأداء والتكلفة."
author: "Omar Albeik"
date: 2026-09-21
type: tutorial
topics: [infrastructure]
language: ar
reading_time_minutes: 26
canonical_url: https://omaralbeik.com/ar/blog/series/cloudflare/understanding-cloudflare-workers
translation_url: https://omaralbeik.com/en/blog/series/cloudflare/understanding-cloudflare-workers
source_url: https://omaralbeik.com/ar/blog/series/cloudflare/understanding-cloudflare-workers.md
---

# دليل Cloudflare Workers: من أول طلب إلى تطبيق يعمل

في [دليل DNS](/ar/blog/series/cloudflare/understanding-cloudflare-dns)، تعرّفنا على إعداد النطاق وربطه بالتطبيق. نكمل هنا من جهة التطبيق نفسه: أين تعمل الشيفرة التي تستقبل الطلب، وكيف نكتبها وننشرها؟

**Cloudflare Workers** بيئة لتشغيل شيفرة الخادم. يستقبل تطبيقك، الذي نسميه Worker، الطلب وينفذ الشيفرة ثم يعيد الاستجابة. يمكنك بناء واجهة برمجية (API) صغيرة عليه، أو معالجة بيانات نموذج، أو تشغيل الجزء الخاص بالخادم من موقعك. وهذا الموقع نفسه يعمل على Workers.

سنبني باستخدام Hono واجهة تقدّر مدة قراءة النصوص العربية والإنجليزية. ترسل إليها نصا، فتعيد عدد كلماته والوقت المتوقع لقراءته. يمكنك إضافة هذه الأداة إلى محرر مقالات؛ وكل ما تحتاجه هنا هو الشيفرة نفسها، من دون قاعدة بيانات أو خدمة خارجية.

للمتابعة، تحتاج إلى أساسيات JavaScript، وطرفية، وإصدار حديث طويل الدعم (LTS) من Node.js مع npm. سنشرح ما نستخدمه من أنواع TypeScript أثناء العمل. يمكنك تجربة المثال محليا دون حساب Cloudflare؛ ستحتاج إليه عند النشر. أما شراء نطاق فاختياري.

- [فهم بيئة التشغيل](#كيف-يعمل-worker).
- [بناء الواجهة واختبارها](#إنشاء-المشروع).
- [النشر وربط النطاق](#نشر-التطبيق).
- [الإعدادات وبيانات الاعتماد والتخزين](#الإعدادات-وربط-الخدمات-عبر-env).
- [حدود الاستخدام والتكلفة](#زمن-المعالجة-ومدة-انتظار-الاستجابة).
- [تتبع الطلبات وتشخيص الأخطاء](#تتبع-الطلب-في-السجلات).

## كيف يعمل Worker؟

تتولى Cloudflare إدارة الأجهزة التي تشغّل التطبيق. تنشر أنت الشيفرة وتحدد الموارد التي يمكنها الوصول إليها، ويتلقى الزائر الاستجابة التي تنتجها. تظل شيفرة Worker على الخادم.

تعمل شيفرة JavaScript داخل **بيئات V8 معزولة (isolates)**. قد تعالج البيئة الواحدة عدة طلبات، ثم تستبدلها المنصة ببيئة جديدة. لذلك لا يمكنك الاعتماد على استمرارها أو بقاء ما وضعته في ذاكرتها بين طلب وآخر.

لنفترض أنك أضفت عدادا للزيارات في متغير عام. يبدو أنه يعمل على جهازك: تحدّث الصفحة فيزداد العدد. لكن في بيئة الإنتاج، قد يصل الطلب إلى بيئة معزولة أخرى لها عدادها الخاص، أو إلى بيئة جديدة يبدأ عدادها من الصفر. يشرح [دليل بيئة التشغيل](https://developers.cloudflare.com/workers/reference/how-workers-works/) هذا النموذج بالتفصيل.

**الشيفرة واحدة، والذاكرة منفصلة**

لنفترض أن لدينا عدادا للزيارات في متغير عام، يبدأ من الصفر.

1. **البيئة المعزولة A — 2**: وصل إليها الطلبان الأول والثاني، فارتفع العداد إلى 2.
2. **البيئة المعزولة B — 1**: وصل الطلب الثالث إلى بيئة أخرى، يبدأ عدادها من الصفر.
3. **بيئة معزولة جديدة — 1**: وصل الطلب الرابع بعد استبدال البيئة A. بدأ العداد الجديد من الصفر أيضا.

وصلت أربعة طلبات، لكن أيا من العدادات لا يحمل مجموعها. هذا مثال محتمل؛ لا تضمن المنصة إعادة استخدام البيئة نفسها لكل طلب.

*لا يمكنك معرفة مجموع طلبات التطبيق من عدّاد في ذاكرة بيئة معزولة واحدة.*

لهذا، احتفظ ببيانات كل طلب داخل دالة معالجته. يمكن وضع الثوابت خارج الدالة، أما البيانات التي تريد الاحتفاظ بها بين الطلبات فتحتاج إلى تخزين دائم.

توفر Workers واجهات ويب مألوفة مثل `Request` و`Response` و`URL` و`fetch()`، لكن من دون صفحة ويب أو شجرة DOM. يستقبل التطبيق طلب HTTP عبر دالة `fetch` التي يصدّرها، ويعيد كائن `Response` بوصفه استجابة الطلب:

```ts
export default {
  async fetch(request, env, ctx): Promise<Response> {
    return new Response("The Worker answered.");
  },
} satisfies ExportedHandler<CloudflareBindings>;
```

تستقبل [دالة المعالجة](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/) ثلاثة كائنات:

- `request`: عنوان الطلب وطريقته وترويساته ومحتواه، كما أرسلها العميل.
- `env`: المتغيرات والقيم السرية والموارد التي ربطتها بالتطبيق في الإعدادات، مثل قواعد البيانات.
- `ctx`: أدوات لإدارة المهام المرتبطة بالطلب الحالي، ومنها إتاحة وقت قصير لإكمال بعض الأعمال بعد إرسال الاستجابة.

يتيح `ExportedHandler<CloudflareBindings>` لـ TypeScript التحقق من توقيع الدالة. سنولّد تعريف `CloudflareBindings` من ملف الإعدادات بعد قليل. هذه الأنواع تساعدنا أثناء التطوير، ثم تُحذف عند البناء؛ ما تشغّله Cloudflare هو JavaScript.

**من وصول الطلب إلى إرسال الاستجابة**

نتتبع طلبا ناجحا إلى واجهة تقدير مدة القراءة التي سنبنيها.

1. **العميل — POST /api/reading-time**: يرسل العميل النص واللغة بصيغة JSON، ويمكنه تحديد سرعة القراءة أيضا.
2. **Cloudflare — اختيار التطبيق**: تحدد إعدادات اسم المضيف تطبيق Worker الذي يستقبل الطلب.
3. **دالة معالجة الطلب — التحقق ← العد**: يختار Hono دالة المعالجة بحسب المسار، فتتحقق من JSON وتعدّ الكلمات.
4. **العودة إلى العميل — 200 · application/json**: يتلقى العميل الحالة والترويسات ونتيجة الحساب بصيغة JSON. إذا كانت المدخلات غير صالحة، نعيد 400 عند التحقق منها.

يحسب التطبيق النتيجة من النص المرسل، دون الرجوع إلى قاعدة بيانات أو خادم آخر.

*تحدد إعدادات التوجيه تطبيق Worker، ويحدد المسار داخل التطبيق الدالة التي تعالج الطلب.*

انتبه إلى الفرق بين استخدامين للاسم `fetch`: الدالة في المثال تستقبل الطلب الوارد، أما استدعاء `fetch()` داخلها فيرسل طلبا إلى خدمة أخرى. لن نحتاج إلى اتصال خارجي في واجهة تقدير القراءة؛ سنحسب النتيجة مباشرة من النص المرسل.

بهذا تكتمل صلة Workers بما شرحناه عن DNS: يساعد DNS العميل على الوصول إلى الخدمة، ويحدد توجيه HTTP تطبيق Worker المقصود، ثم تختار شيفرتك ما يحدث عند طلب `/api/reading-time`. إعداد DNS وحده لا ينشئ هذا المسار داخل التطبيق.

### هل يمكن استخدام حزم npm؟

نعم، بشرط أن تعمل الحزمة والحزم التي تعتمد عليها ضمن بيئة Workers. تدعم طبقة التوافق مع Node.js كثيرا من وحداته المدمجة، لكن الدعم جزئي لبعض الواجهات، وبعضها يمكن استيراده دون أن تتوفر وظائفه. لذلك اختبر الحزمة في بيئة Workers؛ نجاح تثبيتها وحده لا يكفي.

يُفعّل التوافق مع Node.js افتراضيا عندما يكون تاريخ التوافق **4 آب 2026** أو أحدث، وهو ما ينطبق على مثالنا. قد ترى في أمثلة أقدم الخيار `nodejs_compat` مضافا صراحة. يشرح [دليل التوافق](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) هذا الاختلاف. وتظل تجربة الوظائف التي تحتاجها ضرورية، خاصة إذا كانت الحزمة تعتمد على إمكانات نظام التشغيل.

## إنشاء المشروع

رأينا كيف تستقبل دالة واحدة الطلب وتعيد الاستجابة. سنستخدم الآن [Hono](https://hono.dev/docs/getting-started/cloudflare-workers) لتنظيم التطبيق: نعرّف مسارا لكل وظيفة، ونضيف دوال وسيطة (middleware) لفحص الطلبات. تتولى Workers تشغيل هذه الشيفرة ونشرها وإتاحة الموارد المرتبطة بها.

افتح الطرفية في مجلد مشاريعك، ثم نفّذ:

```bash
npm create cloudflare@latest -- reading-api
```

اختر **Hello World**، ثم **Worker only** و**TypeScript**، واختر عدم النشر في هذه الخطوة. تتكفل [أداة إنشاء المشروع](https://developers.cloudflare.com/workers/get-started/guide/) بتثبيت Wrangler، الأداة التي سنستخدمها للتشغيل المحلي والنشر. ادخل إلى مجلد المشروع وأضف Hono:

```bash
cd reading-api
npm install hono
```

سنفصل العمل بين ملفين: `src/analyze.ts` لعدّ الكلمات وحساب المدة، و`src/index.ts` لاستقبال طلبات HTTP والرد عليها. لنبدأ بالإعدادات؛ استبدل محتوى `wrangler.jsonc` بما يلي:

```jsonc title="wrangler.jsonc"
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "reading-api",
  "main": "src/index.ts",
  "compatibility_date": "2026-09-21",
  "workers_dev": true,
  "vars": {
    "SERVICE_NAME": "reading-api",
    "DEFAULT_WPM": 200
  },
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  }
}
```

يحدد `name` اسم التطبيق، و`main` الملف الذي يبدأ منه التنفيذ. يتيح `workers_dev` الوصول إلى التطبيق عبر عنوان عام بعد النشر. وتفعّل `observability` حفظ السجلات؛ اخترنا هنا تسجيل جميع الطلبات بوضع `head_sampling_rate` عند `1`. سنعود إلى السجلات لاحقا.

أما تاريخ التوافق فيحدد سلوك بيئة التشغيل. ابدأ بتاريخ اليوم، واختبر التطبيق إذا غيّرت هذا التاريخ لاحقا. إذا كان إصدار Wrangler لا يدعمه، فحدّث Wrangler داخل المشروع. تاريخ التوافق مستقل عن إصدارات حزم npm؛ تلك يحفظها ملف القفل.

حددنا سرعة القراءة الافتراضية في `DEFAULT_WPM`. **اخترنا 200 كلمة في الدقيقة للتجربة، لا بوصفها متوسطا مقاسا للقراءة بالعربية أو الإنجليزية.** يستطيع العميل إرسال سرعة مختلفة بين 80 و600 كلمة في الدقيقة. وضعنا هذا النطاق في تطبيقنا؛ وهو ليس من حدود Cloudflare.

ولّد الآن تعريفات TypeScript من الإعدادات:

```bash
npx wrangler types --env-interface CloudflareBindings
```

أعد تشغيل الأمر كلما غيّرت الإعدادات. يستخدم Hono التعريف الناتج للتحقق من أنواع القيم التي نقرأها عبر `c.env`، ومنها `DEFAULT_WPM` الرقمية.

## جرب أداة تقدير مدة القراءة

قبل كتابة الشيفرة، جرّب النتيجة التي نريد الوصول إليها. ألصق فقرة واختر لغتها، ثم غيّر سرعة القراءة لترى أثرها في المدة. افتح تفاصيل الطلب والاستجابة إذا أردت رؤية شكل JSON الذي ستتعامل معه الواجهة.

**كم تستغرق قراءة هذا النص؟**

جرّب نصا عربيا أو إنجليزيا، وغيّر سرعة القراءة لترى أثرها في المدة.

1. **POST /api/reading-time؟ — 404 / 405**: المسار المجهول يعيد 404. وأي طريقة أخرى على مسار التحليل تعيد 405 مع Allow: POST.
2. **هل جسم الطلب ضمن حد 64 كيبيبايت؟ — 413**: ترفض الدالة الوسيطة الطلب إذا تجاوز حجمه الحد، قبل تحليل JSON.
3. **هل نوع المحتوى وصيغة JSON والحقول صحيحة؟ — 415 / 400**: نعيد 415 إذا كان نوع المحتوى غير مقبول، و400 إذا كان الخلل في JSON أو النص أو اللغة أو السرعة.
4. **عدّ الكلمات وحساب المدة — 200**: تتضمن النتيجة اللغة وعدد الكلمات وسرعة القراءة والمدة بالثواني والدقائق.

وضعنا السرعة الافتراضية عند 200 كلمة في الدقيقة للتجربة. يمكنك تغييرها؛ فهي ليست متوسطا مقاسا للقراءة.

*تعمل أداة القراءة داخل المتصفح. افتح مخطط التحقق لترى الفحوص الإضافية التي سيجريها الخادم عند استقبال طلب HTTP.*

يحدد اختيار اللغة قواعد تقسيم النص إلى كلمات؛ ولا يترجم النص أو يتحقق من اللغة المكتوب بها. ويمكنك تجربة نص يجمع العربية والإنجليزية. أما المدة فهي تقديرية، لأن صعوبة النص وإلمام القارئ بموضوعه يؤثران في سرعة قراءته.

## عدّ الكلمات وتقدير مدة القراءة

نبدأ بدالة مستقلة عن HTTP: تتحقق من النص واللغة والسرعة، ثم تعدّ الكلمات وتحسب المدة. هذا هو الجزء الذي شغّلته الأداة السابقة داخل متصفحك. أنشئ الملف `src/analyze.ts`:

```ts title="src/analyze.ts"
export function analyzeReading(input: unknown, defaultWpm = 200) {
  if (!input || typeof input !== "object" || Array.isArray(input)) {
    return { ok: false as const, error: "Send a JSON object." };
  }

  const data = input as Record<string, unknown>;

  if (typeof data.text !== "string" || !data.text.trim()) {
    return { ok: false as const, error: "Add some text to analyze." };
  }

  if (data.text.length > 10_000) {
    return {
      ok: false as const,
      error: "Keep text within 10,000 UTF-16 code units.",
    };
  }

  if (data.language !== "en" && data.language !== "ar") {
    return { ok: false as const, error: "Choose en or ar." };
  }

  const wordsPerMinute =
    data.wordsPerMinute === undefined
      ? defaultWpm
      : data.wordsPerMinute;

  if (
    typeof wordsPerMinute !== "number" ||
    !Number.isInteger(wordsPerMinute) ||
    wordsPerMinute < 80 ||
    wordsPerMinute > 600
  ) {
    return {
      ok: false as const,
      error: "Use a whole-number reading speed from 80 to 600.",
    };
  }

  const segmenter = new Intl.Segmenter(data.language, {
    granularity: "word",
  });
  let words = 0;

  for (const segment of segmenter.segment(data.text)) {
    if (segment.isWordLike) {
      words++;
    }
  }

  if (words === 0) {
    return {
      ok: false as const,
      error: "Add text containing at least one word.",
    };
  }

  return {
    ok: true as const,
    value: {
      language: data.language,
      words,
      wordsPerMinute,
      seconds: Math.ceil((words / wordsPerMinute) * 60),
      minutes: Math.ceil(words / wordsPerMinute),
    },
  };
}
```

نستخدم [`Intl.Segmenter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter) لتقسيم النص بحسب قواعد اللغة المختارة، ونعدّ المقاطع التي تحمل `isWordLike`. بذلك لا تدخل المسافات وعلامات الترقيم وحدها في العدد. لا يجري هذا تحليلا صرفيا للعربية؛ فالحروف المتصلة بالكلمة ليست بالضرورة كلمات مستقلة في الحساب. وقد تختلف بعض النتائج قليلا بين بيئات التشغيل، بحسب إصدار بيانات اللغة فيها.

تقبل الدالة قيمة من النوع `unknown` لأن أنواع TypeScript لا تفحص JSON عند وصوله. نتحقق من البيانات بأنفسنا قبل الحساب. كذلك، يقيس `text.length` طول النص بوحدات UTF-16: الحد هنا 10,000 وحدة، لا 10,000 بايت أو حرف ظاهر. فالرمز التعبيري الواحد قد يشغل أكثر من وحدة.

نقرّب قيمتي `seconds` و`minutes` إلى العدد الصحيح الأعلى، كل قيمة على حدة. مثلا، يعطي حساب خمس كلمات بسرعة 200 كلمة في الدقيقة مدة قدرها 1.5 ثانية. نقرّبها إلى ثانيتين، أو إلى دقيقة واحدة إذا اخترنا عرض المدة بالدقائق. لهذا تعرض الأداة السابقة الثواني، حتى تكون النتيجة مفيدة للنصوص القصيرة.

## إضافة المسارات باستخدام Hono

لدينا الآن دالة الحساب. سنستخدمها في تطبيق له مساران: أحدهما للتأكد من أن التطبيق يستجيب، والآخر لاستقبال النص وإعادة النتيجة.

يوفر Hono لكل طلب [كائن سياق](https://hono.dev/docs/api/context) نسميه `c`. نقرأ الطلب عبر `c.req`، ونصل إلى إعدادات Workers ومواردها عبر `c.env`، ونعيد JSON باستخدام `c.json()`. أما `Variables` فيصف القيم التي نحفظها للطلب الحالي عبر `c.set()`، مثل معرّف الطلب، ولا يعرّف إعدادات البيئة.

استبدل محتوى `src/index.ts` بما يلي:

```ts title="src/index.ts"
import { Hono } from "hono";
import { bodyLimit } from "hono/body-limit";
import { analyzeReading } from "./analyze";

const app = new Hono<{
  Bindings: CloudflareBindings;
  Variables: {
    requestId: string;
  };
}>();

app.use("*", async (c, next) => {
  const requestId = crypto.randomUUID();

  c.set("requestId", requestId);
  c.header("X-Request-Id", requestId);
  c.header("Cache-Control", "no-store");

  await next();

  console.log({
    event: "reading_api_response",
    requestId,
    status: c.res.status,
  });
});

app.get("/health", (c) => {
  return c.json({ status: "ok", service: c.env.SERVICE_NAME });
});

app.post(
  "/api/reading-time",
  bodyLimit({
    maxSize: 64 * 1024,
    onError: (c) =>
      c.json({ error: "Keep the request body within 64 KiB." }, 413),
  }),
  async (c) => {
    const mediaType = c.req
      .header("Content-Type")
      ?.split(";")[0]
      ?.trim()
      .toLowerCase();

    if (mediaType !== "application/json") {
      return c.json(
        { error: "Use Content-Type: application/json." },
        415,
      );
    }

    let input: unknown;

    try {
      input = await c.req.json();
    } catch (error) {
      if (error instanceof SyntaxError) {
        return c.json({ error: "Send valid JSON." }, 400);
      }

      throw error;
    }

    const defaultWpm = c.env.DEFAULT_WPM;

    if (
      !Number.isInteger(defaultWpm) ||
      defaultWpm < 80 ||
      defaultWpm > 600
    ) {
      throw new Error("Invalid DEFAULT_WPM configuration.");
    }

    const result = analyzeReading(input, defaultWpm);

    if (!result.ok) {
      return c.json({ error: result.error }, 400);
    }

    return c.json(result.value);
  },
);

app.all("/api/reading-time", (c) => {
  c.header("Allow", "POST");
  return c.json({ error: "Use POST." }, 405);
});

app.notFound((c) => c.json({ error: "Not found." }, 404));

app.onError((error, c) => {
  console.error({
    event: "reading_api_error",
    requestId: c.get("requestId"),
    name: error.name,
  });

  return c.json({ error: "Internal server error." }, 500);
});

export default app;
```

تتكون الشيفرة من ثلاثة أجزاء:

1. **إعداد الطلب وتسجيل نتيجته:** تنشئ الدالة الوسيطة معرّفا جديدا للطلب وتضبط `no-store`، ثم تسجل حالة الاستجابة بعد `await next()`. يبقى النص المرسل خارج السجل.
2. **معالجة المسارات:** يتيح `GET /health` التأكد من أن التطبيق يستجيب، ويستقبل `POST /api/reading-time` النص لتحليله.
3. **معالجة الأخطاء:** نعيد استجابة واضحة للمسارات المجهولة وطرق HTTP غير المدعومة والاستثناءات غير المتوقعة.

تمنع [الدالة الوسيطة `bodyLimit`](https://hono.dev/docs/middleware/builtin/body-limit) تجاوز حجم جسم الطلب 64 كيبيبايت، أي 65,536 بايت، قبل تحليل JSON. يشمل الحجم النص وصيغة JSON المحيطة به. تعتمد الدالة على ترويسة `Content-Length` عند وجودها، وتفحص تدفق البيانات عند غيابها. إذا تجاوز الطلب الحد، تعيد `413` قبل الوصول إلى بقية الفحوص. وهذا حد مستقل عن طول النص الذي فحصناه في دالة الحساب.

بعد فحص الحجم، نشترط نوع المحتوى `application/json` وإلا نعيد `415`. وإذا كانت صيغة JSON أو قيم الحقول غير صالحة، نعيد `400`. ولأي طريقة HTTP أخرى على مسار التحليل، تعيد `app.all()` الحالة `405` مع `Allow: POST`. المسارات المجهولة تعيد `404`. ويستجيب Hono لطلب `HEAD` على `/health` دون محتوى، بينما يظل مسار التحليل مخصصا لـ POST.

إذا وقع استثناء غير متوقع داخل التطبيق، تعيد `app.onError()` الحالة `500` برسالة عامة، وتسجل اسم الخطأ ومعرّف الطلب دون النص المرسل. لكن بعض أخطاء المنصة، مثل استنفاد زمن المعالجة، قد تقع خارج نطاق هذه الدالة. سنناقش حدود التشغيل بعد النشر.

## اختبار الواجهة محليا

شغّل التطبيق على جهازك:

```bash
npx wrangler dev --local
```

يشغّل Wrangler الشيفرة على جهازك باستخدام `workerd`، بيئة تشغيل Workers. يمنع `--local` استخدام الموارد البعيدة عبر إعدادات الربط، لكنه لا يمنع الشيفرة من إرسال طلبات `fetch()` إلى خدمات حقيقية. مثالنا لا يرسل مثل هذه الطلبات. يوضح [دليل التطوير المحلي](https://developers.cloudflare.com/workers/local-development/) هذا الفرق.

افتح نافذة طرفية أخرى وأرسل نصا:

```bash
curl -i 'http://localhost:8787/api/reading-time' \
  -H 'Content-Type: application/json' \
  --data '{"text":"Small APIs can be useful.","language":"en"}'
```

ينبغي أن تحصل على الحالة `200`، وترويستي `Cache-Control: no-store` و`X-Request-Id`، والنتيجة التالية:

```json title="الاستجابة"
{
  "language": "en",
  "words": 5,
  "wordsPerMinute": 200,
  "seconds": 2,
  "minutes": 1
}
```

لتجربة العربية، أرسل نصا عربيا وغيّر `language` إلى `ar`. ويمكنك إضافة `"wordsPerMinute": 150` لتغيير سرعة القراءة. بعد نجاح الطلب، جرّب الحالات التي ينبغي أن ترفضها الواجهة:

```bash
# Health check: 200
curl -i 'http://localhost:8787/health'

# Empty text: 400
curl -i 'http://localhost:8787/api/reading-time' \
  -H 'Content-Type: application/json' \
  --data '{"text":"","language":"en"}'

# Invalid JSON: 400
curl -i 'http://localhost:8787/api/reading-time' \
  -H 'Content-Type: application/json' \
  --data '{'

# Wrong content type: 415
curl -i 'http://localhost:8787/api/reading-time' \
  --data 'hello'

# Wrong method: 405, with Allow: POST
curl -i 'http://localhost:8787/api/reading-time'

# Unknown route: 404
curl -i 'http://localhost:8787/missing'
```

إذا أردت تجربة مقال كامل، فاحفظ محتوى الطلب بصيغة JSON في `request.json`، واستخدم `--data-binary @request.json` مع ترويسة نوع المحتوى نفسها. بهذا ترسل نص UTF-8 كما هو، وتتجنب مشكلات علامات الاقتباس في الطرفية. اختبر أيضا لغة غير مدعومة، ونصا لا يحتوي إلا على علامات ترقيم، وسرعة بقيمة كسرية أو خارج النطاق، وجسم طلب يتجاوز 64 كيبيبايت.

المثال متاح دون مصادقة. تطلب `no-store` عدم تخزين الاستجابة مؤقتا، لكنها لا تحدد من يحق له استدعاء الواجهة. وإذا استدعيتها من واجهة أمامية على أصل مختلف، فستحتاج إلى إعداد CORS للسماح لها بقراءة الاستجابة. تنظم CORS هذا الوصول داخل المتصفح، ولا تتحقق من هوية المتصل.

## نشر التطبيق

**متى يصبح التطبيق متاحا للناس؟**

تعمل في الخطوات الثلاث الأولى على جهازك. عند الخطوة الرابعة يصبح التطبيق متاحا عبر عنوان عام.

1. **01 · كتابة الشيفرة — src/index.ts + wrangler.jsonc**: يحتوي المشروع على دالة معالجة الطلبات وإعدادات تشغيلها.
2. **02 · التشغيل المحلي — wrangler dev --local**: جرّب إرسال طلبات HTTP إلى التطبيق على جهازك.
3. **03 · فحص حزمة النشر — wrangler deploy --dry-run**: تأكد من إمكانية بناء الحزمة دون نشر التطبيق.
4. **04 · النشر — wrangler deploy**: انشر التطبيق، ثم جرّب عنوانه الذي يظهر في الطرفية.

يتحقق dry-run من بناء الحزمة. وللتأكد من التوجيه والإعدادات بعد النشر، أرسل طلبا إلى العنوان المنشور.

*يبقى التطبيق المنشور كما هو أثناء العمل المحلي. لا تصل إليه الشيفرة الجديدة إلا عند تنفيذ أمر النشر.*

بعد تجربة الطلبات، تحقق من أنواع TypeScript وابن حزمة التطبيق للتأكد من جاهزيتها للنشر:

```bash
npx tsc --noEmit
npx wrangler deploy --dry-run
```

يبني الأمر `deploy --dry-run` الحزمة دون نشرها. لكنه لا يتحقق من صلاحيات بيئة الإنتاج أو إعدادات النطاقات أو الخدمات البعيدة التي قد تضيفها لاحقا.

لنشر المثال، سجّل الدخول ثم نفّذ أمر النشر:

```bash
npx wrangler login
npx wrangler deploy
```

يعرض Wrangler عنوان التطبيق بعد النشر، وغالبا يكون بالشكل `https://reading-api.<your-subdomain>.workers.dev`. استخدم العنوان الذي ظهر لك في الطرفية: اختبر `/health` أولا، ثم أعد إرسال أمثلة POST إلى `/api/reading-time` على العنوان الجديد. يمكنك استخدام هذا العنوان مباشرة دون شراء نطاق.

تؤكد هذه الخطوة أن الطلب يصل إلى التطبيق المنشور بالحساب والإعدادات المقصودة. نجاح التجربة المحلية وحده لا يتحقق من ذلك.

### ربط نطاقك الخاص

في مثالنا، ينشئ Worker الاستجابة النهائية بنفسه، فيؤدي دور خادم الأصل. يمكنك ربطه **بنطاق مخصص (Custom Domain)** ليستقبل الطلبات الموجهة إلى اسم مضيف مثل `reading.example.com`. إذا كان نطاقك مفعّلا على Cloudflare، فأضف `routes` إلى المستوى الأعلى من `wrangler.jsonc`، واستبدل الاسم باسم مضيف تملكه:

```jsonc title="wrangler.jsonc · إضافة إلى الإعدادات الحالية"
{
  "routes": [
    {
      "pattern": "reading.example.com",
      "custom_domain": true
    }
  ]
}
```

أعد النشر بعد تعديل الإعدادات. تتولى Cloudflare إدارة سجل DNS وشهادة الاتصال المشفر [للنطاق المخصص](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/). اختر اسم مضيف غير مستخدم؛ فقد يتعارض سجل CNAME موجود مع هذا الإعداد. ويظل اختيار المسار داخل التطبيق مسؤولية Hono، فيصبح عنوان واجهتنا `https://reading.example.com/api/reading-time`.

أما **Route** فتستخدمه عندما يكون لديك خادم أصل بالفعل وتريد تشغيل Worker أمامه، كأن يفحص الطلب قبل تمريره إلى الخادم. يطابق Route نمطا في العنوان، ويتطلب سجل DNS مناسبا مع تفعيل وكيل Cloudflare. يشرح [دليل التوجيه](https://developers.cloudflare.com/workers/configuration/routing/routes/) هذا الاستخدام. في مثالنا، يكفي النطاق المخصص لأن Worker ينتج الاستجابة بنفسه.

## الإعدادات وربط الخدمات عبر env

قرأنا اسم الخدمة من `c.env.SERVICE_NAME` وسرعة القراءة الافتراضية من `c.env.DEFAULT_WPM`. ويمكن أن يتيح `env` أيضا قيما سرية أو وصولا إلى موارد مثل قواعد البيانات وحاويات الملفات:

**ما الذي يتيحه env لشيفرتك؟**

المثال الأول قيمة إعداد استخدمناها في الواجهة. أما البقية فتوضح ما قد تضيفه لاحقا.

1. **c.env.SERVICE\_NAME — قيمة إعداد**: اسم الخدمة كما حددناه في vars: reading-api. لا يحتاج إلى إخفاء.
2. **c.env.PROVIDER\_API\_KEY — قيمة سرية**: بيانات اعتماد تستخدمها الشيفرة عند الاتصال بخدمة خارجية.
3. **c.env.DB — ربط بقاعدة بيانات**: وصول إلى قاعدة D1 محددة في الإعدادات، مع صلاحية استخدامها.
4. **c.env.UPLOADS — ربط بحاوية ملفات**: وصول إلى حاوية R2 محددة. تبقى الملفات مخزنة في R2.

يتيح الربط الوصول إلى المورد؛ وتبقى قاعدة البيانات والملفات في خدمات التخزين، خارج ذاكرة التطبيق.

*قد تجد في env قيمة تقرؤها مباشرة، أو واجهة تتعامل عبرها مع مورد ربطته بالتطبيق، مثل قاعدة بيانات.*

نسمي هذا [ربطا (binding)](https://developers.cloudflare.com/workers/runtime-apis/bindings/): تحدد المورد في الإعدادات، فتتيح Workers للشيفرة التعامل معه. مثلا، إذا ربطت قاعدة D1 باسم `DB`، أمكنك الاستعلام عنها عبر `c.env.DB` دون تضمين رمز API لحساب Cloudflare في الشيفرة. أضف إعداد الربط ثم أعد توليد الأنواع.

لا تحتاج أداة تقدير القراءة إلى حفظ النصوص. لكن إذا طوّرتها إلى محرر مقالات، فاختر التخزين بحسب البيانات وطريقة استخدامها:

- **D1** للسجلات التي تريد الاستعلام عنها باستخدام SQL، مثل المقالات المحفوظة.
- **KV** للقيم التي تقرؤها بمفاتيحها، عندما يكون التأخر في ظهور التحديثات مقبولا.
- **R2** للملفات، مثل المستندات المصدرة.
- **Durable Objects** لتنسيق العمل على كائن واحد، كأن يعدّل محرران المستند نفسه.

لكل خيار سلوكه وحدوده. حدد ما يحتاجه التطبيق قبل الاختيار؛ حفظ قيمة بين الطلبات ليس المعيار الوحيد. تفيدك [مقارنة خيارات التخزين](https://developers.cloudflare.com/workers/platform/storage-options/) هنا، وسنفصل هذه الخيارات في مقالات لاحقة من السلسلة.

### حفظ بيانات الاعتماد بأمان

إذا احتجت لاحقا إلى بيانات اعتماد لخدمة خارجية، فاحفظها كقيمة سرية:

```bash
npx wrangler secret put PROVIDER_API_KEY
```

سيطلب Wrangler إدخال القيمة، وتقرأها الشيفرة بعد ذلك عبر `c.env.PROVIDER_API_KEY`. أثناء التطوير المحلي، ضعها في `.dev.vars` وأضف النمطين `.dev.vars*` و`.env*` إلى `.gitignore`.

لا يرفع Wrangler القيم المحلية تلقائيا إلى بيئة الإنتاج؛ أضفها هناك بأمر `secret put`. يوضح [دليل القيم السرية](https://developers.cloudflare.com/workers/configuration/secrets/) كيفية إعدادها لكل بيئة.

لا نحتاج إلى هذه الخطوة في مثالنا، لأن الأداة لا تتصل بخدمة خارجية.

### فصل البيئة التجريبية عن الإنتاج

قبل تجربة تغييرات على تطبيق منشور، قد تحتاج إلى نسخة تجريبية مستقلة عنه. تتيح [بيئات Wrangler](https://developers.cloudflare.com/workers/wrangler/environments/) نشر المشروع نفسه كتطبيقات Workers منفصلة. أضف `env` إلى الإعدادات الحالية:

```jsonc title="wrangler.jsonc · إضافة إلى الإعدادات الحالية"
{
  "env": {
    "staging": {
      "routes": [],
      "vars": {
        "SERVICE_NAME": "reading-api-staging",
        "DEFAULT_WPM": 200
      }
    }
  }
}
```

استخدم `--env staging` عند تشغيل النسخة التجريبية أو نشرها:

```bash
npx wrangler dev --local --env staging
npx wrangler deploy --env staging
```

ينشئ أمر النشر هنا تطبيقا باسم `reading-api-staging`. تمنع قائمة `routes` الفارغة استخدام نطاق الإنتاج الذي أضفناه سابقا؛ اختبر النسخة التجريبية على عنوان `workers.dev` الخاص بها. وتبقى إعدادات المستوى الأعلى خاصة بالتطبيق الأصلي `reading-api`. لذلك حدد البيئة المقصودة عند تنفيذ أوامر تغيّر التطبيق المنشور.

لا ترث البيئة التجريبية متغيرات المستوى الأعلى أو إعدادات ربط الموارد؛ عرّفها لكل بيئة على حدة. أضف القيم السرية لكل بيئة أيضا، باستخدام `--env staging` عند الحاجة. وانتبه إلى أن فصل التطبيقين لا يفصل بياناتهما إذا ربطتهما بقاعدة البيانات نفسها.

**قاعدة بيانات مستقلة لكل بيئة**

إذا أضفت D1 لاحقا، فخصص قاعدة بيانات لكل بيئة. يمكن للتطبيقين استخدام اسم الربط نفسه: c.env.DB.

1. **البيئة التجريبية — reading-api-staging**: يصل c.env.DB إلى قاعدة بيانات للاختبار: reading-api-staging-db.
2. **الإنتاج — reading-api**: يصل c.env.DB إلى قاعدة بيانات الإنتاج: reading-api-production-db.

هذه أسماء توضيحية لقواعد قد نضيفها لاحقا. إذا ربطت التطبيقين بالقاعدة نفسها، فسيتعاملان مع البيانات نفسها.

*استخدام اسم الربط نفسه في البيئتين لا يعني استخدام قاعدة البيانات نفسها؛ تحدد الإعدادات المورد المقصود في كل بيئة.*

## زمن المعالجة ومدة انتظار الاستجابة

بعد تشغيل التطبيق، كيف تقيس الوقت الذي يستهلكه؟ تخيل طلبا تمر معالجته بثلاث خطوات: التحقق من المدخلات، وانتظار قاعدة بيانات، ثم إعداد الاستجابة. لنفترض أن التحقق يستهلك 3 مللي ثانية من عمل المعالج، والانتظار 200 مللي ثانية، وإعداد الاستجابة 2 مللي ثانية.

استغرقت المعالجة نحو 205 مللي ثانية داخل الدالة، لكن **زمن المعالجة الفعلي على المعالج (CPU time) كان 5 مللي ثانية فقط**. هذه أرقام توضيحية لفهم الفرق، وليست قياسا لأداء واجهتنا.

**طلب واحد، وقياسان للوقت**

غيّر مدة انتظار البيانات، ولاحظ أن زمن المعالجة يبقى ثابتا.

1. **التحقق — 3 مللي ثانية معالجة**: تنفيذ فحوص المدخلات.
2. **انتظار البيانات — 200 مللي ثانية انتظار**: يزيد انتظار الشبكة مدة الطلب، من دون أن يزيد زمن معالجة Worker.
3. **التنسيق — 2 مللي ثانية معالجة**: إعداد الاستجابة.

عند انتظار 200 مللي ثانية: زمن المعالجة 5 مللي ثانية، والوقت داخل دالة المعالجة نحو 205 مللي ثانية. هذه أرقام توضيحية وليست نتائج قياس، ولا تشمل بقية وقت الاتصال عبر الشبكة.

*قد ينتظر المستخدم طويلا رغم أن الشيفرة لا تستهلك إلا زمنا قصيرا من عمل المعالج. قِس زمن الاستجابة أيضا.*

نحتاج إلى هذا الفرق لفهم حدود الخطة وتكلفتها. بحسب [حدود Workers](https://developers.cloudflare.com/workers/platform/limits/) بتاريخ 21 أيلول 2026، تنطبق الحدود التالية على معالجة طلبات HTTP العادية:

| الحد | Workers Free | Workers Paid |
| --- | --- | --- |
| الطلبات الواردة | 100,000 يوميا | الفوترة بحسب الاستخدام |
| زمن المعالجة لكل طلب | 10 مللي ثانية | 30 ثانية افتراضيا، ويمكن رفعه إلى 5 دقائق |
| الذاكرة لكل بيئة معزولة | 128 ميغابايت | 128 ميغابايت |

تتشارك تطبيقات الحساب حصة الخطة المجانية اليومية، وتتجدد عند منتصف الليل بتوقيت UTC. لا يحتسب انتظار الشبكة ضمن زمن المعالجة، لكن ذلك لا يضمن استمرار الطلب إلى ما لا نهاية: إذا قطع العميل الاتصال، فقد تُلغى الأعمال التي لم تنته.

كذلك تتشارك الطلبات داخل البيئة المعزولة ذاكرتها. إذا تعاملت مع محتوى كبير، فاستخدم [البث المتدفق (streaming)](https://developers.cloudflare.com/workers/runtime-apis/streams/) لتمريره تدريجيا بدلا من تحميله كله في الذاكرة. أما واجهتنا فتعيد كائن JSON صغيرا، وتناسبها `c.json()` في Hono.

### ماذا لو كانت قاعدة البيانات بعيدة؟

تشغّل Cloudflare تطبيق Worker افتراضيا قرب نقطة دخول الطلب إلى شبكتها. يناسب ذلك مثالنا، لأن كل ما نحتاجه موجود في الطلب نفسه. أما إذا كان التطبيق يتصل مرارا بقاعدة بيانات بعيدة، فقد يقضي معظم وقته في انتظارها.

تتيح إعدادات [موضع التنفيذ (Placement)](https://developers.cloudflare.com/workers/configuration/placement/) تشغيل التطبيق أقرب إلى خدماته الخلفية إذا كان ذلك يقلل زمن الاستجابة الإجمالي. قِس مدة الطلب والاتصالات الخارجية قبل تغيير موضع التنفيذ؛ فالقرب من المستخدم لا يكفي وحده.

### هل يمكن إكمال مهمة بعد إرسال الاستجابة؟

إذا كانت الاستجابة تعتمد على نتيجة عملية غير متزامنة، فانتظرها باستخدام `await`. أما الأعمال القصيرة التي لا تتوقف عليها الاستجابة، فيمكن منحها وقتا إضافيا عبر `ctx.waitUntil()`، أو `c.executionCtx.waitUntil()` في Hono.

تصل [المهلة الموثقة](https://developers.cloudflare.com/workers/runtime-apis/context/) لطلبات HTTP إلى 30 ثانية بعد إرسال الاستجابة أو انقطاع اتصال العميل. تتشارك جميع مهام `waitUntil()` التابعة للطلب نفسه هذه المهلة. وإذا بدأت عملية تعيد `Promise` دون انتظارها أو تمريرها إلى `waitUntil()`، فقد تتوقف قبل أن تكتمل.

للمهام التي تحتاج إلى تسليم موثوق وإعادة المحاولة عند الفشل، استخدم Queue. وللعمليات التي تتكون من خطوات وفترات انتظار وتحتاج إلى حفظ تقدمها، استخدم [Workflow](https://developers.cloudflare.com/workflows/). وفي كل الأحوال، لا ترسل «تم الحفظ» قبل اكتمال الكتابة في قاعدة البيانات؛ نجاحها لاحقا أثناء التجربة المحلية لا يضمن أن يحدث ذلك في الإنتاج.

## كم تبلغ التكلفة؟

ابدأ بالخطة المجانية لتجربة الواجهة، ثم قِس استهلاك المعالج وعدد الطلبات لتعرف متى تحتاج إلى خطة مدفوعة.

وقت كتابة المقال، يبدأ الاشتراك في [خطة Workers Standard](https://developers.cloudflare.com/workers/platform/pricing/) من **5 دولارات أمريكية للحساب شهريا**. يشمل ذلك 10 ملايين طلب و30 مليون مللي ثانية من زمن المعالجة. وتبلغ تكلفة الاستخدام الزائد 0.30 دولار لكل مليون طلب، و0.02 دولار لكل مليون مللي ثانية من المعالجة.

**كيف نحسب تكلفة 12 مليون طلب؟**

عند افتراض 4 مللي ثانية من المعالجة لكل طلب، نستهلك 48 مليون مللي ثانية. نضيف إلى الاشتراك الأساسي تكلفة ما يتجاوز الحصة المشمولة.

1. **الاشتراك الأساسي — $5.00**: يشمل 10 ملايين طلب و30 مليون مللي ثانية من المعالجة.
2. **الطلبات الإضافية — $0.60**: مليونا طلب فوق الحصة المشمولة، بسعر 0.30 دولار لكل مليون.
3. **المعالجة الإضافية — $0.36**: 18 مليون مللي ثانية إضافية، بسعر 0.02 دولار لكل مليون.

إجمالي تكلفة الحوسبة: 5.96 دولارات. نفترض توفر الحصة المشمولة كاملة، ولا نحسب الضرائب أو السجلات أو التخزين أو الخدمات الأخرى.

*يمثل الجزء الملون الحصة المشمولة بالاشتراك، والجزء ذو الخطوط المائلة الاستخدام الزائد. يعتمد الحساب على أسعار 21 أيلول 2026 المذكورة أعلاه.*

يفترض المثال أن الحصة المشمولة بالاشتراك متاحة كاملة لهذا التطبيق، وأن التطبيقات الأخرى في الحساب لم تستهلك منها شيئا. ولا يشمل الضرائب أو السجلات أو التخزين أو الخدمات الأخرى. أما متوسط 4 مللي ثانية لكل طلب فافتراض للحساب، وليس قياسا لأداء المثال.

الاشتراك الأساسي هو بداية الفاتورة، وليس سقفا لها. راقب الاستخدام والسجلات مع زيادة عدد الطلبات؛ فالاستجابة الصغيرة قد تُطلب ملايين المرات.

## تتبع الطلب في السجلات

أثناء تشغيل `wrangler dev`، تظهر سجلات المثال في الطرفية. وبعد النشر، يمكنك متابعة السجلات مباشرة بالأمر:

```bash
npx wrangler tail
```

أرسل طلبا، وقارن قيمة ترويسة الاستجابة `X-Request-Id` مع `requestId` في سجل `reading_api_response`. ولمتابعة البيئة التجريبية، استخدم `npx wrangler tail --env staging`.

تجد السجلات المحفوظة في صفحة [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/) بلوحة التحكم، بعد تفعيل `observability`. عندما تكون `head_sampling_rate` مساوية لـ `1`، تُختار جميع الطلبات للتسجيل؛ وعند `0.1` تُختار نحو 10% منها. يقلل أخذ العينات حجم السجلات، لكنه يعني أنك قد لا تجد سجلا لطلب محدد.

للسجلات حدود استخدام ومدة احتفاظ وتسعير خاص بها. ويكتب مثالنا حدثا في السجل من داخل الشيفرة، إضافة إلى حدث الاستدعاء الذي تسجله المنصة. لذلك قد ينتج عن الطلب الواحد أكثر من حدث.

إذا واجهت مشكلة، فابدأ بما يظهر لك في الاستجابة أو السجل:

| ما الذي تراه؟ | ما الذي تفحصه أولا؟ |
| --- | --- |
| `400` مع رسالة JSON من تطبيقنا | صيغة JSON والنص واللغة وسرعة القراءة |
| `404` مع رسالة JSON من تطبيقنا | المسار؛ `/health` أو `/api/reading-time` |
| `413` أو `415` | حجم جسم الطلب أو ترويسة Content-Type |
| لا يوجد سجل تطبيق مطابق | العنوان والتطبيق المستهدف والبيئة ونسبة أخذ العينات |
| قيمة ربط تساوي `undefined` | اسم الربط وإعدادات البيئة المختارة |
| الخطأ `1102` لتجاوز الموارد | استهلاك المعالج والذاكرة في الاستدعاء الذي فشل |
| زمن معالجة منخفض واستجابة بطيئة | اتصالات الشبكة وتأخر الخدمات الخلفية |

عند حدوث استثناء داخل التطبيق، ابحث عن قيمة `X-Request-Id` في سجل `reading_api_error`. ستجد اسم الخطأ، بينما يتلقى العميل رسالة عامة مع الحالة `500`. مثلا، القيمة غير الصحيحة لـ `DEFAULT_WPM` خطأ في إعدادات التطبيق؛ لذلك نعيد `500` بدلا من `400` التي تدل على مشكلة في بيانات العميل. وإذا لم تصل استجابة من التطبيق أصلا، فافحص أخطاء التنفيذ واستهلاك الموارد في المنصة.

## العودة إلى إصدار سابق

عند نشر تغيير، من المفيد أن تعرف كيف ترجع عنه. يسجّل إصدار Worker الشيفرة والإعدادات، وتحدد عملية النشر أي إصدار يستقبل الطلبات. اعرض الإصدارات الأخيرة بالأمر:

```bash
npx wrangler versions list
```

إذا تعطلت الواجهة بعد نشر تغيير، فاختر إصدارا سبق نشره وتأكدت من عمله، ثم [ارجع إليه](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/):

```bash
npx wrangler rollback <VERSION_ID>
```

استبدل `<VERSION_ID>` بالمعرّف الفعلي، وأضف `--env staging` إذا كانت البيئة التجريبية هي المقصودة. أعد اختبارات HTTP على العنوان المنشور بعد الرجوع.

العودة إلى إصدار سابق تعيد الشيفرة، لكنها لا تتراجع عن عمليات الكتابة في قاعدة البيانات ولا تستعيد الموارد المحذوفة. وقد يتعذر الرجوع بعد حذف مورد مرتبط بالتطبيق أو إجراء بعض تغييرات Durable Objects. لذلك، عندما تضيف بيانات دائمة، ضع خطة مستقلة لاستعادتها وتأكد من توافق بنيتها مع الشيفرة التي قد ترجع إليها.

## ماذا تبني بعد ذلك؟

أصبح لديك تطبيق يعمل على Workers، ويمكنك تتبع ما يحدث فيه من وصول الطلب إلى ظهور نتيجته في السجل. فصلنا حساب المدة عن معالجة HTTP، واستخدمنا Hono لتنظيم المسارات، ثم جرّبنا الواجهة وتناولنا نشرها ومتابعة تشغيلها.

لتوسيع المثال، جرّب إتاحة تحليل عدة مسودات في طلب واحد. حدد عدد المسودات وحجم الطلب الإجمالي، وقرر ما يحدث إذا كانت إحداها غير صالحة: هل ترفض الدفعة كلها أم تعيد نتيجة مستقلة لكل مسودة؟ طبّق القرار واختبره.

في الجزء المقبل، سنضيف واجهة أمامية ونناقش خيارات استضافة المواقع على Workers، وما يقدمه Pages في هذا الجانب.

إذا كان هذا الدليل سيساعد أحد زملائك في بناء أول واجهة API على Workers، فشاركه معه. وما الجزء الذي احتجت وقتا أطول لفهمه في تجربتك: بيئة التشغيل، أم ربط الخدمات، أم النشر؟ [راسلني بتجربتك](/ar/contact)، أو شارك المقال مع ملاحظاتك على X أو LinkedIn.
