السلسلة: هذا الدرس جزء من سلسلة NestJS 11. اقرأ المقال الرئيسي.
monorepo Nx 22 اليوم أنجع هيكل لمنتج ينطلق بـ API NestJS ويخطّط لإضافة عميل web أو mobile سريعاً. بدل إدارة 3 مستودعات git، 3 خطوط CI و3 مجموعات تبعيات تنحرف مع الوقت، نُشارك workspace واحداً، libs types مشتركة، وأمر nx affected الذي يُعيد بناء ما تغيّر فقط.
المتطلبات
- Node.js 22.x LTS (Jod)
- pnpm 9.x — موصى به للـ monorepos (ربح 40% على install مقابل npm)
- Git 2.40+
- حساب GitHub
- المستوى: متوسط
- 60-90 دقيقة
الخطوة 1 — إنشاء workspace Nx
pnpm dlx create-nx-workspace@22.6.5 acme \
--preset=apps \
--packageManager=pnpm \
--nxCloud=skip
cd acme
الخيار --preset=apps ينشئ workspace أدنى بلا framework مفروض. --nxCloud=skip يُعطّل دعوة Nx Cloud. النتيجة: مجلد acme/ يحوي nx.json، package.json جذر ومجلدَي apps/ وlibs/ فارغين. nx graph يفتح واجهة web ترسم رسماً بيانياً للمشاريع.
الخطوة 2 — إضافة plugin Nest وتوليد التطبيق api
pnpm add -D @nx/nest@22.6.5
nx g @nx/nest:application api \
--linter=eslint \
--unitTestRunner=jest \
--e2eTestRunner=jest
المولّد ينشئ apps/api/ ببنية NestJS المعيارية (main.ts، app.module.ts، إلخ) ومشروع apps/api-e2e/ لاختبارات التكامل. nx serve api يطلق الخادم على المنفذ 3000. للتحقق: curl http://localhost:3000/api يعيد {"message":"Hello API"}.
الخطوة 3 — إنشاء مكتبة types مشتركة
الفائدة الرئيسية لـ monorepo مشاركة الكود بين عدة تطبيقات. مكتبة shared-types تحوي DTO وenums المستخدَمة من backend والعملاء المستقبليين تتجنّب تكرار الواجهة في مستودعين وانحرافها بمرور الوقت.
pnpm add -D @nx/js@22.6.5
nx g @nx/js:library shared-types \
--directory=libs/shared-types \
--bundler=tsc \
--unitTestRunner=jest
المكتبة في libs/shared-types/src/ مع index.ts يُصدّر ما تستهلكه المشاريع الأخرى. الاستيراد جانب apps/api بـ import { UserDto } from '@acme/shared-types' بفضل حلّ المسارات في tsconfig.base.json.
الخطوة 4 — تفعيل TypeScript strict وESLint مشترك
مشروع ينطلق بلا "strict": true يراكم ديون تنميط مكلفة لاحقاً. تفعيل strict منذ أول commit يفرض كود دفاعياً يكشف bugs عند build لا في الإنتاج.
// tsconfig.base.json (مقتطف)
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
الخيار noUncheckedIndexedAccess غالباً الأفيد لـ backend: يفرض التحقق أن وصولاً لمصفوفة أو كائن قد يُرجع undefined. يلغي عائلة كاملة من bugs «cannot read property of undefined».
الخطوة 5 — تقسيم API إلى modules أعمال
NestJS يشجّع التقسيم حسب المجال لا الطبقة التقنية. بدل مجلد controllers/، services/، dtos/ تخلط كل الميزات، ننشئ مجلداً لكل module أعمال.
nx g @nx/nest:module billing --project=api
nx g @nx/nest:controller billing --project=api --module=billing
nx g @nx/nest:service billing --project=api --module=billing
الثلاثة أوامر تنشئ apps/api/src/billing/ مع billing.module.ts، billing.controller.ts وbilling.service.ts. الـ module BillingModule لا يكشف خارجياً إلا ما يُعلنه في exports — افتراضياً لا شيء.
الخطوة 6 — ضبط CI GitHub Actions مع nx affected
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
affected:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v5
with: { node-version: 22 }
- uses: pnpm/action-setup@v6
with: { version: 9 }
- run: pnpm install --frozen-lockfile
- run: npx nx affected -t lint test build --base=origin/main
fetch-depth: 0 إلزامي: بلا تاريخ git كامل، Nx لا يستطيع حساب قاعدة المقارنة ويُعيد بناء كل شيء. --base=origin/main يدلّ على فرع المرجع.
الخطوة 7 — التحقق من الرسم البياني وتثبيت التبعيات
nx graph # واجهة web على localhost:4211
nx report # إصدارات plugins
nx affected:graph # رسم بياني للمتأثّرة منذ main
إن رفع nx report plugin بإصدار مختلف عن نواة Nx، الأمر nx migrate latest يُحاذي الكل ويطبّق codemods الضرورية تلقائياً. عملية يجب فعلها كل 3 أشهر للبقاء على patches الأمنية.
الخطوة 8 — الاستفادة من cache Nx المحلي
Nx يُخزّن مخرَج مهام build، test وlint في .nx/cache/. تنفيذ ثانٍ على نفس commit، حتى بعد reboot، يستعيد المخرَج فوراً بلا إعادة تنفيذ.
nx build api # المرة الأولى: 8 ثوان
# عدّل ملف غير ذي صلة
nx build api # المرة الثانية: cache hit، 200 مللي ثانية
nx reset # تفريغ cache عند الحاجة
الآلية تعتمد hash محسوب من ملفات إدخال المشروع وتبعياته. إن لم يتغيّر hash، يُستعاد المخرَج.
ممارسات تنظيم libs
بدل مكتبة واحدة shared-types تنتهي بحمل كل شيء، نظّم libs حسب scope ونوع. scope يطابق المجال (billing، users، orders)، النوع يطابق طبيعة الكود (data-access، feature، ui، util). مكتبة billing-data-access تحوي استدعاءات API والأنواع؛ billing-feature تحوي منطق الأعمال؛ billing-ui تحوي المكوّنات.
قيود الاستيراد تُعلَن في .eslintrc.json عبر القاعدة @nx/enforce-module-boundaries. مكتبة موسومة type:util لا تستطيع استهلاك إلا مكتبات type:util أخرى.
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
| NX 0001 عند generate | Plugin Nx غير مُحاذٍ مع core | nx migrate latest |
Import @acme/shared-types غير مُحلَّل |
Path alias غائب من tsconfig.base.json | أعد توليد lib أو أضف المسار |
| CI تُعيد البناء على كل commit | fetch-depth مفقود |
أضف fetch-depth: 0 إلى checkout |
| تعارض pnpm-lock بين الفروع | إصدارات pnpm مختلفة | ثبّت packageManager في package.json |
nx serve api لا يُعيد التحميل |
Watch SWC سيء الإعداد | تحقّق --watch في project.json |
أسئلة شائعة
لماذا pnpm بدل npm أو yarn؟ pnpm يستخدم store عالمياً وروابط رمزية بدل نسخ الحزم. على monorepo بـ 5 مشاريع، install ينتقل من 90 ثانية (npm) إلى 30 ثانية (pnpm).
هل نُفعّل Nx Cloud؟ لمشروع ناشئ، لا. cache محلي وكشف affected يغطّيان 90% من الربح. Nx Cloud يصير ملائماً عندما يتجاوز الفريق 5 مطوّرين.
هل يمكن خلط NestJS وNext.js في نفس workspace؟ نعم، حتى استخدام شائع. أضف plugin @nx/next. الفخّ الوحيد عزل متغيّرات البيئة جيداً: NEXT_PUBLIC_* جانب front، API_* جانب backend.
كيف نُهاجر مشروع NestJS موجود نحو Nx؟ الأمر nx init يكتشف مشروعاً موجوداً ويقترح هجرة تدريجية تحفظ scripts npm وبنية المجلدات.