بنية تحتية تبدو كأنك تستورد وحدة وتستدعي دالة. Alchemy مكتبة بنية تحتية كَكود (IaC) مبنية أصلاً على TypeScript — دون YAML أو HCL أو لغة وصف منفصلة. الموارد مجرد دوال غير متزامنة تنتظرها بـ await. في هذا الدرس ستبني وتنشر واجهة برمجية كاملة على Cloudflare Workers مدعومة بقاعدة بيانات D1 ومساحة أسماء KV وحاوية R2 وأسرار مُحكمة الأنواع — كل ذلك من ملف alchemy.run.ts واحد.
ما الذي ستتعلمه
بنهاية هذا الدرس ستكون قادراً على:
- فهم ماهية Alchemy وكيف تختلف عن Terraform وPulumi وSST وWrangler
- تهيئة مشروع Alchemy والمصادقة مع Cloudflare
- نشر Cloudflare Worker مكتوب بلغة TypeScript خالصة
- توفير وربط قاعدة بيانات D1 ومساحة أسماء KV وحاوية R2
- إدارة الأسرار بأمان عبر
alchemy.secret - تشغيل حلقة تطوير محلية تُحاكي روابطك
- فحص وفهم ملفات الحالة ودور
finalize() - هدم كل شيء بأمان عبر أمر واحد
المتطلبات المسبقة
قبل البدء، تأكّد من توفّر:
- Node.js 20+ أو Bun 1.1+ مثبّت (
node --version) - حساب Cloudflare مجاني (لا حاجة لخطة مدفوعة في هذا الدرس)
- إلمام أساسي بـ TypeScript ونمط async/await
- محرّر أكواد (يُنصح بـ VS Code)
لا حاجة لأي خبرة سابقة في Terraform أو Pulumi أو Wrangler. بل إن نسيانها قد يساعدك.
ما هو Alchemy؟
Alchemy مكتبة بنية تحتية كَكود (IaC) قابلة للتضمين، بلا تبعيات، ومبنية أصلاً على TypeScript. بينما تطلب منك معظم أدوات IaC تعلّم لغة إعداد — مثل HCL في Terraform أو YAML في CloudFormation أو تجريدات SDK في Pulumi — لا يطلب منك Alchemy تعلّم شيء جديد. المورد مجرد دالة غير متزامنة:
const bucket = await R2Bucket("uploads");هذا السطر الواحد يُنشئ حاوية R2 إن لم تكن موجودة، ويُحدّثها إن تغيّر إعدادها، ويسجّل حالتها محلياً. لا حفلة plan/apply، ولا إضافات مزوّدين لتثبيتها، ولا مستوى تحكّم خفي. النموذج الذهني بكل بساطة: استدعِ دالة، واحصل على مورد.
كيف تُقارن
| الأداة | اللغة | النموذج الذهني | الحالة |
|---|---|---|---|
| Terraform | HCL | رسم بياني تعريفي | .tfstate بعيد/محلي |
| Pulumi | SDK بلغات TS/Go/Python | رسم بياني عبر SDK | خدمة Pulumi |
| SST | TS فوق محرك Pulumi | بِنى (Constructs) | مُدارة/محلية |
| Wrangler | wrangler.toml | سطر أوامر + ملف إعداد | مُدارة من Cloudflare |
| Alchemy | TypeScript خالص | دوال غير متزامنة | ملفات محلية يمكنك حفظها |
أبرز الفروق التي تجعل Alchemy متميزاً في 2026:
- الموارد دوال غير متزامنة. لا طبقة تجريد جديدة للتعلّم.
- TypeScript بصيغة ESM خالصة. يعمل أينما عملت JavaScript الحديثة، مع دعم متميز لـ Bun.
- الحالة داخل مستودعك. ملفات الحالة محلية وقابلة للقراءة البشرية، يمكن فحصها وتعديلها وحفظها في git.
- متعدّد المزوّدين. Cloudflare وAWS من الدرجة الأولى، ويمكنك توليد مزوّد لأي واجهة REST بسرعة.
يركّز هذا الدرس على Cloudflare لأنه أسرع طريق من الصفر إلى واجهة برمجية منشورة ذات حالة.
الخطوة 1: تهيئة المشروع
أسرع بداية هي أمر create الذي يُعدّ مشروع TypeScript ويُثبّت Alchemy ويربط السكربتات:
# باستخدام npm
npx alchemy@latest create alchemy-api --template typescript
# باستخدام Bun (مُوصى به — Alchemy يحبّ Bun)
bunx alchemy@latest create alchemy-api --template typescriptانتقل إلى المجلد:
cd alchemy-apiيمنحك القالب بنية مشابهة لهذه:
alchemy-api/
├── alchemy.run.ts # بنيتك التحتية — قلب المشروع
├── src/
│ └── worker.ts # كود تطبيق الـ Worker
├── package.json
├── tsconfig.json
└── .env # الأسرار المحلية (متجاهَلة في git)الملفان المهمّان هما alchemy.run.ts (ماذا تنشر) وsrc/worker.ts (الكود الذي يعمل). هذا الفصل — البنية التحتية والتطبيق يعيشان جنباً إلى جنب بنفس اللغة — هو جوهر الفكرة كلها.
الخطوة 2: المصادقة مع Cloudflare
يحتاج Alchemy إلى إذن لإدارة الموارد في حساب Cloudflare لديك. شغّل تسجيل الدخول التفاعلي مرة واحدة:
npx alchemy loginيُنفّذ هذا تدفّق OAuth في متصفّحك ويخزّن رمزاً محلياً. في بيئات CI يمكنك تخطّي الدخول التفاعلي وتوفير متغيّر بيئة CLOUDFLARE_API_TOKEN بدلاً منه — وسيلتقطه Alchemy تلقائياً.
إن احتجت يوماً لإعادة ضبط إعدادات المشروع، شغّل:
npx alchemy configureالخطوة 3: انشر أول Worker
افتح alchemy.run.ts. في أبسط صوره، يُهيّئ الملف تطبيقاً، ويعلن الموارد، ثم يُنهي. استبدل المحتوى بالتالي:
import alchemy from "alchemy";
import { Worker } from "alchemy/cloudflare";
// تهيئة التطبيق. الاسم ينشئ مساحة أسماء لكل الموارد والحالة.
const app = await alchemy("alchemy-api");
const worker = await Worker("api", {
entrypoint: "./src/worker.ts",
});
console.log(`Worker deployed at: ${worker.url}`);
// finalize يحفظ الحالة ويزيل الموارد اليتيمة.
await app.finalize();الآن اكتب الـ Worker الفعلي في src/worker.ts:
export default {
async fetch(request: Request): Promise<Response> {
return Response.json({
message: "Hello from Alchemy!",
timestamp: new Date().toISOString(),
});
},
};انشره:
npx alchemy deployيُنشئ Alchemy الـ Worker، ويرفع كودك المُجمّع، ويطبع رابط *.workers.dev حياً. افتحه — ستجد ردّ JSON خاصتك. لقد شحنت للتو واجهة برمجية بلا خوادم دون أي ملفات إعداد.
ماذا حدث للتو؟ فتح
alchemy("alchemy-api")«نطاقاً» يتتبّع كل مورد تنشئه. كل استدعاءawait Worker(...)يوفّق الحالة المرغوبة مع الواقع. ثم يكتبapp.finalize()ملف الحالة ويحذف أي شيء موجود في الحالة لكنه لم يَعُد معلَناً في الكود. هكذا يجمع Alchemy «نفايات» الموارد المحذوفة.
الخطوة 4: أضف قاعدة بيانات D1
أي واجهة حقيقية تحتاج تخزيناً دائماً. Cloudflare D1 قاعدة بيانات SQLite بلا خوادم، ويوفّرها Alchemy كأي مورد آخر. حدّث alchemy.run.ts:
import alchemy from "alchemy";
import { Worker, D1Database } from "alchemy/cloudflare";
const app = await alchemy("alchemy-api");
// توفير قاعدة بيانات D1.
const db = await D1Database("app-db", {
name: "alchemy-app-db",
});
const worker = await Worker("api", {
entrypoint: "./src/worker.ts",
// الروابط تُعرّض الموارد للـ Worker عبر الاسم.
bindings: {
DB: db,
},
});
console.log(`Worker deployed at: ${worker.url}`);
await app.finalize();كائن bindings هو الجسر بين البنية التحتية ووقت التشغيل. المفتاح DB يصبح متاحاً داخل الـ Worker بصيغة env.DB، مُحكم النوع كعميل D1. لا wrangler.toml، ولا تصاريح روابط يدوية.
تحديد أنواع بيئة الـ Worker
يستنتج Alchemy أنواع الروابط من alchemy.run.ts. صدّر الـ worker لتتدفّق أنواع بيئته إلى كود تطبيقك. أضف تصديراً في alchemy.run.ts:
export { worker };ثم في src/worker.ts، استورد نوع البيئة المُستنتَج:
import type { worker } from "../alchemy.run.ts";
type Env = typeof worker.Env;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/users") {
// env.DB عميل D1 مُحكم النوع — دون توصيل أنواع يدوي.
const { results } = await env.DB.prepare(
"SELECT id, name FROM users LIMIT 10",
).all();
return Response.json({ users: results });
}
return Response.json({ message: "Hello from Alchemy!" });
},
};اسم الرابط في alchemy.run.ts والخاصية على env مضمونٌ تطابقهما لأنهما يأتيان من مصدر حقيقة واحد. أعِد تسمية رابط فيُشير TypeScript إلى كل موضع استخدام.
الخطوة 5: أضف روابط KV وR2
تحتاج معظم التطبيقات أكثر من قاعدة بيانات — التخزين المؤقت وتخزين الكائنات شائعان. يتعامل Alchemy مع مساحات أسماء KV وحاويات R2 تماماً كـ D1: أعلِن ثم اربط.
import alchemy from "alchemy";
import {
Worker,
D1Database,
KVNamespace,
R2Bucket,
} from "alchemy/cloudflare";
const app = await alchemy("alchemy-api");
const db = await D1Database("app-db", { name: "alchemy-app-db" });
const cache = await KVNamespace("app-cache");
const uploads = await R2Bucket("app-uploads");
const worker = await Worker("api", {
entrypoint: "./src/worker.ts",
bindings: {
DB: db,
CACHE: cache,
UPLOADS: uploads,
},
});
console.log(`Worker deployed at: ${worker.url}`);
export { worker };
await app.finalize();الآن الموارد الثلاثة متاحة داخل الـ Worker بصيغة env.CACHE وenv.UPLOADS، ومُحكمة الأنواع مجدداً:
import type { worker } from "../alchemy.run.ts";
type Env = typeof worker.Env;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// KV: مثال تخزين مؤقت بالقراءة المسبقة
if (url.pathname === "/config") {
const cached = await env.CACHE.get("config");
if (cached) return new Response(cached);
const fresh = JSON.stringify({ theme: "dark", version: 2 });
// تخزين لمدة ساعة واحدة
await env.CACHE.put("config", fresh, { expirationTtl: 3600 });
return new Response(fresh);
}
// R2: تخزين ملف مرفوع
if (url.pathname === "/upload" && request.method === "POST") {
const key = `uploads/${Date.now()}`;
await env.UPLOADS.put(key, request.body);
return Response.json({ stored: key });
}
return Response.json({ message: "Hello from Alchemy!" });
},
};أعِد النشر بـ npx alchemy deploy. يقارن Alchemy مواردك المعلَنة بالحالة المسجّلة: قاعدة البيانات موجودة فعلاً فتُترك كما هي، بينما تُنشأ وتُربَط مساحة أسماء KV وحاوية R2 الجديدتان. هذا التوفيق التزايدي هو ما يجعل IaC آمنة للتشغيل مراراً.
الخطوة 6: إدارة الأسرار بأمان
يجب ألا تبقى مفاتيح الـ API والرموز نصاً صريحاً في كودك أو ملفات حالتك. يوفّر Alchemy الدالة alchemy.secret التي تُشفّر القيم قبل كتابتها في الحالة وتحقنها في الـ Worker وقت التشغيل.
const worker = await Worker("api", {
entrypoint: "./src/worker.ts",
bindings: {
DB: db,
CACHE: cache,
UPLOADS: uploads,
// غلّف أي قيمة حساسة لتُشفّر أثناء التخزين في الحالة.
API_KEY: alchemy.secret(process.env.STRIPE_KEY),
},
});يمكنك أيضاً السحب مباشرة من متغيّرات البيئة عبر المساعد .env الذي يقرأ المتغيّر المُسمّى ويفشل بوضوح إن كان مفقوداً:
bindings: {
// يقرأ process.env.STRIPE_KEY، يُشفّره، ويخطئ إن لم يكن مضبوطاً.
API_KEY: alchemy.secret.env.STRIPE_KEY,
},داخل الـ Worker، السرّ نصّ عادي على env:
const auth = request.headers.get("authorization");
if (auth !== `Bearer ${env.API_KEY}`) {
return new Response("Unauthorized", { status: 401 });
}خزّن القيمة الفعلية في ملف .env المحلي (الذي يتجاهله القالب في git) وفي مدير أسرار CI للنشر. الصيغة المُشفّرة هي ما يصل إلى الحالة، لذا حفظ الحالة لا يُسرّب الاعتمادات أبداً.
نصيحة: يُشفّر Alchemy الأسرار في الحالة بعبارة مرور. اضبط
ALCHEMY_PASSWORDفي بيئتك للتشغيل المحلي وCI معاً كي يمكن فكّ تشفير نفس الحالة عبر الأجهزة.
الخطوة 7: حلقة التطوير المحلية
النشر إلى Cloudflare عند كل تغيير بطيء. يأتي Alchemy بوضع تطوير يُحاكي روابطك محلياً مع إعادة تحميل فورية:
npx alchemy devتُحاكَى معظم روابط Cloudflare — D1 وKV وR2 — تلقائياً، فيعمل env.DB وenv.CACHE وenv.UPLOADS دون المساس بمواردك الإنتاجية. تُحلّ الأسرار من ملف .env المحلي. عدّل src/worker.ts واحفظ، فيُعاد تحميل الـ Worker فوراً مقابل المحاكاة المحلية.
يمنحك هذا حلقة التغذية الراجعة السريعة لخادم محلي مع الحفاظ على مصدر حقيقة واحد: نفس alchemy.run.ts يصف بيئة تطويرك المحلية والإنتاج. لا انحراف بين wrangler.toml للتطوير وشيء آخر للإنتاج.
الخطوة 8: فهم ملفات الحالة
بعد أول نشر، انظر في مجلد .alchemy/. ستجد ملفات حالة JSON تصف كل مورد يديره Alchemy — المعرّفات والإعدادات والأسرار (مُشفّرة). هذا متعمَّد وليس مخفياً خلف خدمة بعيدة.
ولأن الحالة محلية وقابلة للقراءة البشرية، يمكنك:
- حفظها في إدارة الإصدارات ليتشارك فريقك رؤية متّسقة للبنية المنشورة
- فحصها لفهم ما هو موجود بالضبط ولماذا
- مقارنتها في طلبات الدمج لرؤية أثر تغيير على موارد حقيقية
هذه فلسفة مختلفة عن Terraform Cloud أو الواجهة الخلفية المُدارة في Pulumi. Alchemy يأتمنك على حالتك. للفرق، يمكنك تهيئة مخزن حالة مشترك (مثلاً مدعوم بمورد Cloudflare) كي تبقى عمليات النشر المتزامنة متّسقة — لكن للمشاريع الفردية والفرق الصغيرة، الحالة المحلية المحفوظة كافية غالباً.
الخطوة 9: اهدم كل شيء
من أفضل اختبارات أداة IaC مدى نظافة إزالتها لما صنعت. يتتبّع Alchemy كل مورد في الحالة، فأمر واحد يهدمها جميعاً بترتيب التبعيات:
npx alchemy destroyيحذف هذا الـ Worker وقاعدة بيانات D1 ومساحة أسماء KV وحاوية R2 — كل شيء في نطاق تطبيقك. لا موارد يتيمة تتراكم بصمت على فاتورة Cloudflare. ولأن الهدم مُوجّه بالحالة، فهو دقيق: يُزال فقط ما أنشأه Alchemy.
اختبار تنفيذك
تحقّق من التدفّق كاملاً من البداية للنهاية:
- النشر: شغّل
npx alchemy deployوتأكّد من استجابة الرابط المطبوع. - قاعدة البيانات:
curl https://your-worker.workers.dev/usersيُعيد مصفوفة JSON (فارغة حتى تُدخل بيانات). - التخزين المؤقت:
curl https://your-worker.workers.dev/configمرّتين — الاستدعاء الثاني يُخدَم من KV. - التخزين:
curl -X POST --data-binary @file.png https://your-worker.workers.dev/uploadيُعيد مفتاح تخزين. - السرّ: طلب دون ترويسة
Authorizationالصحيحة إلى مسار محمي يُعيد401. - وضع التطوير: شغّل
npx alchemy dev، عدّل الـ Worker، وتأكّد من عمل إعادة التحميل الفوري مقابل الروابط المُحاكاة. - الهدم:
npx alchemy destroyيُزيل كل شيء؛ إعادة تشغيله لا تفعل شيئاً.
استكشاف الأخطاء وإصلاحها
Not authenticated عند النشر. شغّل npx alchemy login مجدداً، أو اضبط CLOUDFLARE_API_TOKEN في CI. تأكّد أن للرمز صلاحيات Workers وD1 وKV وR2.
الرابط undefined وقت التشغيل. يجب أن يطابق المفتاح في كائن bindings الخاصية التي تقرأها على env. تأكّد أنك صدّرت worker من alchemy.run.ts واستوردت typeof worker.Env كي يلتقط TypeScript عدم التطابق.
تعذّر فكّ تشفير السرّ عبر الأجهزة. اضبط نفس ALCHEMY_PASSWORD في كل مكان تُستخدم فيه الحالة. عبارة مرور مختلفة لا تفكّ تشفير أسرار كُتبت بأخرى.
تعارض الحالة عند النشر من جهازين. الحالة المحلية لكل نسخة عمل. للنشر المشترك، هيّئ مخزن حالة بعيداً بدلاً من حفظ تعديلات متزامنة على نفس ملفات JSON.
destroy يترك مورداً. يعني هذا عادةً أن المورد أُنشئ خارج Alchemy (مثلاً يدوياً في لوحة التحكم). Alchemy يدير فقط ما في حالته.
الخطوات التالية
- أضف Durable Objects وQueues — كلاهما موارد Cloudflare من الدرجة الأولى في
alchemy/cloudflare، تُربط تماماً كـ D1 وKV. - اربط Alchemy بـ CI/CD عبر سير عمل GitHub Actions يشغّل
alchemy deployعند الدمج — راجع دليل GitHub Actions للـ CI/CD. - قارن مع نهج SST في درس نشر SST Ion على AWS.
- انشر تطبيقاً كاملاً على Workers مع درس واجهة Cloudflare Workers + Hono + D1.
- ولّد مزوّداً مخصصاً لواجهة REST داخلية وأدِرها جنباً إلى جنب مع موارد Cloudflare.
الخلاصة
يطوي Alchemy الفجوة بين كود التطبيق والبنية التحتية برفضه إدخال لغة جديدة للأخيرة. الموارد دوال غير متزامنة، والروابط كائنات مُحكمة الأنواع، والحالة JSON قابل للقراءة تملكه أنت، ونفس الملف يصف التطوير المحلي والإنتاج. في هذا الدرس هيّأت مشروعاً، ونشرت Cloudflare Worker، وأرفقت قاعدة بيانات D1 ومساحة أسماء KV وحاوية R2، وأمّنته بأسرار مُشفّرة، وشغّلت حلقة تطوير محلية، وهدمت كل شيء بأمر واحد.
للفرق التي تعيش أصلاً في TypeScript — خاصة على Cloudflare — يزيل Alchemy فئة كاملة من تبديل السياق. لا YAML لتتذكّره، ولا HCL لتتعلّمه، ولا واجهة خلفية مُدارة تعتمد عليها. مجرد كود يبني وينشر وينظّف البنية التحتية التي يحتاجها تطبيقك.