التوثيق الشامل لإطار عمل NestJS

1. مقدمة عن NestJS

NestJS هو إطار عمل مفتوح المصدر لتطوير تطبيقات الخلفية (Backend) باستخدام Node.js. تم إطلاقه في عام 2017 على يد كامل ميسكو (Kamil Myśliwiec)، وسرعان ما اكتسب شعبية كبيرة في مجتمع المطورين.

لماذا NestJS؟

يتميز NestJS بأنه يدمج بين قوة Express.js (أو Fastify) وفلسفة تصميم Angular. هذا يعني أنه يستفيد من سرعة ومرونة Express.js، مع توفير بنية تنظيمية قوية مستوحاة من Angular.

ملاحظة: NestJS ليس إطار عمل جديدًا تمامًا، ولكنه طبقة مبنية فوق Express (الافتراضي) أو Fastify. هذا يعني أنك تستطيع استخدام جميع وحدات وميزات Express.js أو Fastify مع NestJS.

المميزات الرئيسية

  • الهيكلية الموجهة: يوفر NestJS بنية واضحة للتطبيق، مما يجعل التطوير فريقيًا أكثر فعالية.
  • TypeScript بشكل أساسي: مبني على TypeScript، مما يوفر أنظمة إدخال قوية وأدوات تطوير محسّنة.
  • حقن التبعية (Dependency Injection): يستخدم نمط حقن التبعية لإنشاء علاقات بين الكائنات وفصل المسؤوليات.
  • تعدد البروتوكولات: يدعم REST و GraphQL و WebSockets و Microservices وغيرها.
  • قابلية الاختبار: صُمم ليكون قابلاً للاختبار بشكل كامل، مع دعم اختبارات الوحدة والتكامل.
  • مجتمع نشط: يتمتع بمجتمع نشط ومتنامي ودعم جيد.

المفاهيم الأساسية

يعتمد NestJS على ثلاثة مفاهيم رئيسية تشكل نواة أي تطبيق:

1. الوحدات (Modules)

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

2. المتحكمات (Controllers)

تتعامل مع طلبات الواردة من العميل وترسل الاستجابات المناسبة.

3. مزودو الخدمات (Providers)

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

2. تثبيت وإعداد المشروع

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

قبل البدء في استخدام NestJS، تأكد من تثبيت المتطلبات التالية على جهازك:

  • Node.js (الإصدار 12 أو أحدث): يمكن تحميله من nodejs.org
  • npm أو Yarn: npm يأتي مع Node.js، أو يمكنك تثبيت Yarn من خلال npm install -g yarn

تثبيت CLI

الطريقة المفضلة للبدء مع NestJS هي استخدام Nest CLI، وهي أداة سطر أوامر تساعدك في إنشاء وتطوير تطبيقات NestJS:

npm install -g @nestjs/cli

شرح الأمر:

  • npm install -g: تثبيت حزمة بشكل عالمي على النظام
  • @nestjs/cli: حزمة واجهة سطر الأوامر الخاصة بـ NestJS

إنشاء مشروع جديد

بعد تثبيت Nest CLI، يمكنك إنشاء مشروع جديد بالأمر التالي:

nest new project-name

شرح الأمر:

  • nest new: أمر إنشاء مشروع جديد
  • project-name: اسم المشروع الذي تريد إنشاءه (استبدله باسم مشروعك)

سيطلب منك CLI اختيار مدير الحزم الذي تريد استخدامه (npm، Yarn، أو pnpm). هذا الاختيار لا يؤثر على وظائف المشروع، ولكنه يحدد أداة إدارة الحزم التي ستستخدم للمشروع.

نصيحة: يمكنك أيضًا استخدام قوالب جاهزة لبدء مشروعك باستخدام الأمر:
nest new project-name --template=typescript-starter

تشغيل التطبيق

بعد إنشاء المشروع، انتقل إلى مجلد المشروع وقم بتشغيل التطبيق:

cd project-name
npm run start:dev

شرح الأوامر:

  • cd project-name: الانتقال إلى مجلد المشروع
  • npm run start:dev: تشغيل التطبيق في وضع التطوير مع إعادة التحميل التلقائي عند تغيير الملفات

سيبدأ تشغيل الخادم على المنفذ 3000 افتراضياً. يمكنك الوصول إلى التطبيق من خلال المتصفح على العنوان: http://localhost:3000

3. هيكل المشروع

عند إنشاء مشروع جديد باستخدام Nest CLI، يتم إنشاء هيكل ملفات افتراضي. دعنا نتعرف على الملفات والمجلدات الرئيسية وأهميتها:

project-name/
├── src/                       # مجلد المصدر الرئيسي
│   ├── app.controller.spec.ts # ملف اختبار المتحكم
│   ├── app.controller.ts      # المتحكم الرئيسي
│   ├── app.module.ts          # الوحدة الرئيسية
│   ├── app.service.ts         # الخدمة الرئيسية
│   └── main.ts                # نقطة الدخول الرئيسية للتطبيق
├── test/                      # مجلد اختبارات e2e
├── node_modules/              # الوحدات المثبتة
├── nest-cli.json              # إعدادات CLI
├── package.json               # إعدادات المشروع والتبعيات
├── tsconfig.json              # إعدادات TypeScript
├── tsconfig.build.json        # إعدادات بناء TypeScript
└── README.md                  # توثيق المشروع

شرح الملفات الرئيسية

1. src/main.ts

هذا هو نقطة الدخول الرئيسية للتطبيق. يحتوي على دالة bootstrap التي تقوم بإنشاء مثيل لتطبيق NestJS وتشغيله.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  // إنشاء مثيل من تطبيق NestJS
  const app = await NestFactory.create(AppModule);
  // تشغيل التطبيق على المنفذ 3000
  await app.listen(3000);
}
bootstrap();

شرح سطر بسطر:

  • import { NestFactory } from '@nestjs/core';: استيراد NestFactory من الحزمة الأساسية
  • import { AppModule } from './app.module';: استيراد الوحدة الرئيسية للتطبيق
  • async function bootstrap() { ... }: تعريف دالة غير متزامنة (async) لبدء التطبيق
  • const app = await NestFactory.create(AppModule);: إنشاء مثيل من تطبيق NestJS باستخدام AppModule
  • await app.listen(3000);: تشغيل التطبيق على المنفذ 3000
  • bootstrap();: استدعاء الدالة لبدء التطبيق

2. src/app.module.ts

هذا هو الوحدة الرئيسية للتطبيق. تقوم بتجميع جميع المكونات (المتحكمات والخدمات) وتوفير نقطة دخول واحدة لهيكل التطبيق.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

شرح سطر بسطر:

  • import { Module } from '@nestjs/common';: استيراد الواصم (decorator) Module
  • import { AppController } from './app.controller';: استيراد المتحكم الرئيسي
  • import { AppService } from './app.service';: استيراد الخدمة الرئيسية
  • @Module({ ... }): واصم لتعريف وحدة في NestJS
  • imports: []: قائمة الوحدات المستوردة (فارغة حاليًا)
  • controllers: [AppController]: قائمة المتحكمات في هذه الوحدة
  • providers: [AppService]: قائمة مزودي الخدمات في هذه الوحدة
  • export class AppModule {}: تصدير الوحدة لاستخدامها في main.ts

3. src/app.controller.ts

هذا هو المتحكم الرئيسي. المتحكمات مسؤولة عن التعامل مع الطلبات HTTP وإرجاع الاستجابات للعميل.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

شرح سطر بسطر:

  • import { Controller, Get } from '@nestjs/common';: استيراد الواصمات اللازمة
  • import { AppService } from './app.service';: استيراد الخدمة لاستخدامها في المتحكم
  • @Controller(): واصم لتعريف متحكم في NestJS (بدون مسار محدد، سيكون الجذر '/')
  • constructor(private readonly appService: AppService) {}: حقن التبعية للخدمة
  • @Get(): واصم لتعريف معالج طلب GET
  • getHello(): string { ... }: دالة تُنفَّذ عند تلقي طلب GET على المسار الجذر
  • return this.appService.getHello();: استدعاء دالة من الخدمة وإرجاع النتيجة

4. src/app.service.ts

هذه هي الخدمة الرئيسية. الخدمات تحتوي على منطق الأعمال في التطبيق وتُستخدم لفصل هذا المنطق عن المتحكمات.

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

شرح سطر بسطر:

  • import { Injectable } from '@nestjs/common';: استيراد الواصم Injectable
  • @Injectable(): واصم لتعريف مزود خدمة قابل للحقن في NestJS
  • export class AppService { ... }: تعريف صنف الخدمة
  • getHello(): string { ... }: دالة تعيد نص 'Hello World!'

هيكل مشروع موصى به للتطبيقات الكبيرة

للتطبيقات الأكبر حجمًا، يُفضَّل تنظيم الملفات حسب الميزات أو الوحدات:

src/
├── main.ts
├── app.module.ts
├── common/                  # مكونات مشتركة (filters, pipes, guards, interceptors)
│   ├── filters/
│   ├── pipes/
│   └── guards/
│   └── interceptors/
├── config/                  # إعدادات التطبيق
├── modules/                 # المكونات المختلفة للتطبيق
│   ├── users/
│   │   ├── dto/
│   │   ├── entities/
│   │   ├── users.controller.ts
│   │   ├── users.service.ts
│   │   └── users.module.ts
│   ├── products/
│   │   ├── dto/
│   │   ├── entities/
│   │   ├── products.controller.ts
│   │   ├── products.service.ts
│   │   └── products.module.ts
├── database/                # إعدادات قاعدة البيانات
└── utils/                   # أدوات مساعدة

شرح هيكل المشروع:

  • common/: مجلد يحتوي على المكونات المشتركة مثل الفلاتر والأنابيب والحراس والمعترضات
  • config/: مجلد لإعدادات التطبيق المختلفة مثل متغيرات البيئة
  • modules/: مجلد يحتوي على الوحدات الوظيفية المختلفة للتطبيق
  • modules/users/: مثال لوحدة وظيفية كاملة للمستخدمين
  • modules/users/dto/: كائنات نقل البيانات لوحدة المستخدمين
  • modules/users/entities/: كيانات قاعدة البيانات لوحدة المستخدمين
  • database/: مجلد لإعدادات قاعدة البيانات والهجرات
  • utils/: مجلد للوظائف والأدوات المساعدة

4. الوحدات (Modules)

الوحدات هي العنصر الأساسي في بنية تطبيق NestJS. تساعد في تنظيم كود التطبيق وفصل المسؤوليات. كل تطبيق يحتاج على الأقل وحدة جذر واحدة (AppModule).

تعريف الوحدة

يتم تعريف الوحدة باستخدام واصم @Module() الذي يأخذ كائن إعدادات يحتوي على أربع خصائص رئيسية:

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [],          // الوحدات المستوردة
  controllers: [UsersController],  // المتحكمات التي تنتمي لهذه الوحدة
  providers: [UsersService],       // مزودو الخدمات التي تنتمي لهذه الوحدة
  exports: [UsersService],         // مزودو الخدمات التي يمكن استخدامها خارج هذه الوحدة
})
export class UsersModule {}

شرح كائن إعدادات الوحدة:

  • imports: مصفوفة من الوحدات المستوردة، التي تصدر مزودي خدمات مطلوبة في هذه الوحدة
  • controllers: مصفوفة من المتحكمات التي يجب إنشاؤها في هذه الوحدة
  • providers: مصفوفة من مزودي الخدمات التي يجب إنشاؤها وتوفيرها في هذه الوحدة
  • exports: مصفوفة من مزودي الخدمات التي يجب أن تكون متاحة للاستخدام في وحدات أخرى

مثال عملي لهيكل الوحدات

لنأخذ مثالاً لتطبيق به وحدة للمستخدمين ووحدة للمنتجات:

// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './modules/users/users.module';
import { ProductsModule } from './modules/products/products.module';

@Module({
  imports: [UsersModule, ProductsModule],
})
export class AppModule {}

// modules/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // تصدير الخدمة للاستخدام في وحدات أخرى
})
export class UsersModule {}

// modules/products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule], // استيراد وحدة المستخدمين للاستفادة من خدماتها
  controllers: [ProductsController],
  providers: [ProductsService],
})
export class ProductsModule {}

شرح المثال:

  • app.module.ts:
    • وحدة الجذر التي تستورد وحدتي المستخدمين والمنتجات
  • users.module.ts:
    • تعريف وحدة المستخدمين التي تحتوي على متحكم وخدمة
    • تصدير UsersService ليمكن استخدامها في وحدات أخرى
  • products.module.ts:
    • تعريف وحدة المنتجات التي تستورد وحدة المستخدمين
    • يمكن لخدمة المنتجات الآن الوصول إلى UsersService

الوحدات العالمية

يمكنك إنشاء وحدات عالمية تكون متاحة تلقائياً للتطبيق بأكمله دون الحاجة لاستيرادها في كل وحدة. مثال:

import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';

@Global()  // جعل الوحدة عالمية
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

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

5. المتحكمات (Controllers)

المتحكمات مسؤولة عن التعامل مع الطلبات الواردة وإرجاع الاستجابات للعميل. تستخدم لتحديد مسارات API وطرق الطلب ومعالجتها.

تعريف المتحكم

يتم تعريف المتحكم باستخدام واصم @Controller():

import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './interfaces/user.interface';

@Controller('users')  // المسار الأساسي: /users
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()  // GET /users
  findAll(): User[] {
    return this.usersService.findAll();
  }

  @Get(':id')  // GET /users/:id (مثلاً /users/123)
  findOne(@Param('id') id: string): User {
    return this.usersService.findOne(id);
  }

  @Post()  // POST /users
  create(@Body() createUserDto: CreateUserDto): User {
    return this.usersService.create(createUserDto);
  }
}

شرح سطر بسطر:

  • @Controller('users'): يحدد المسار الأساسي للمتحكم
  • constructor(private readonly usersService: UsersService) {}: حقن التبعية للخدمة
  • @Get(): يستجيب لطلبات GET على المسار /users
  • findAll(): User[]: دالة للحصول على جميع المستخدمين
  • @Get(':id'): يستجيب لطلبات GET مع معلمة id (مثل /users/123)
  • findOne(@Param('id') id: string): دالة تستخدم الواصم Param لاستخراج معلمة المسار
  • @Post(): يستجيب لطلبات POST على المسار /users
  • create(@Body() createUserDto: CreateUserDto): دالة تستخدم الواصم Body لاستخراج بيانات الطلب

واصمات طرق HTTP

NestJS يوفر واصمات لجميع طرق HTTP الشائعة:

  • @Get() - لطلبات GET
  • @Post() - لطلبات POST
  • @Put() - لطلبات PUT
  • @Delete() - لطلبات DELETE
  • @Patch() - لطلبات PATCH
  • @Options() - لطلبات OPTIONS
  • @Head() - لطلبات HEAD
  • @All() - لجميع طرق HTTP

واصمات المعلمات

للوصول إلى معلمات مختلفة من الطلب، توفر NestJS عدة واصمات:

import { Controller, Get, Post, Body, Param, Query, Headers, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('example')
export class ExampleController {
  @Get('params/:id')
  withParams(@Param('id') id: string): string {
    return `معلمة المسار: ${id}`;
  }

  @Get('query')
  withQuery(@Query('name') name: string): string {
    return `معلمة الاستعلام: ${name}`;
  }

  @Post()
  withBody(@Body() body: any): string {
    return `جسم الطلب: ${JSON.stringify(body)}`;
  }

  @Get('headers')
  withHeaders(@Headers('user-agent') userAgent: string): string {
    return `ترويسة المستخدم: ${userAgent}`;
  }

  @Get('request')
  withRequest(@Req() request: Request): string {
    return `كائن الطلب الكامل متاح هنا`;
  }
}

شرح واصمات المعلمات:

  • @Param(key?: string): يستخرج معلمات المسار
  • @Query(key?: string): يستخرج معلمات الاستعلام
  • @Body(key?: string): يستخرج جسم الطلب
  • @Headers(name?: string): يستخرج ترويسات الطلب
  • @Req() أو @Request(): يوفر كائن الطلب الكامل
  • @Res() أو @Response(): يوفر كائن الاستجابة

ملاحظة: يُفضَّل استخدام الواصمات المحددة مثل @Body() و @Param() بدلاً من الواصمات العامة مثل @Req() لزيادة قابلية الاختبار والتحديد.

أنواع الاستجابات

هناك عدة طرق للتعامل مع الاستجابات في NestJS:

import { Controller, Get, Post, HttpCode, Header, Redirect, Res } from '@nestjs/common';
import { Response } from 'express';

@Controller('responses')
export class ResponsesController {
  @Get('standard')
  standard(): string {
    return 'استجابة قياسية';  // استجابة نصية مع رمز 200
  }

  @Get('object')
  object(): object {
    return { message: 'استجابة كائن' };  // يتم تحويلها تلقائيًا إلى JSON
  }
  
  @Post('status')
  @HttpCode(201)  // تغيير رمز الحالة إلى 201
  status(): string {
    return 'تم إنشاء العنصر';
  }
  
  @Get('headers')
  @Header('Cache-Control', 'none')  // إضافة ترويسة مخصصة
  headers(): string {
    return 'استجابة مع ترويسات مخصصة';
  }
  
  @Get('redirect')
  @Redirect('https://nestjs.com', 301)  // إعادة توجيه مع رمز 301
  redirect() {}
  
  @Get('custom')
  custom(@Res() res: Response) {
    return res
      .status(202)
      .header('Custom-Header', 'Value')
      .send('استجابة مخصصة بالكامل');
  }
}

شرح أنواع الاستجابات:

  • إرجاع قيمة بسيطة (نص، كائن) يتم تحويلها تلقائيًا إلى استجابة مناسبة
  • @HttpCode() لتحديد رمز حالة HTTP
  • @Header() لإضافة ترويسات مخصصة
  • @Redirect() لإعادة التوجيه
  • استخدام @Res() للوصول المباشر إلى كائن الاستجابة وتخصيصه بالكامل

6. مزودو الخدمات (Providers)

مزودو الخدمات هم المفهوم الأساسي في NestJS. يمكن اعتبار الكثير من الصفوف الأساسية في NestJS كمزودي خدمات: الخدمات، المخازن، الأنظمة، وغيرها. الفكرة الأساسية لمزود الخدمة هو أنه يمكن حقنه كتبعية، مما يعني أن الكائنات يمكن أن تنشئ علاقات مختلفة مع بعضها البعض.

تعريف مزود الخدمة

يتم تعريف مزود الخدمة عادةً باستخدام واصم @Injectable():

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
  
  findOne(id: string): Cat {
    return this.cats.find(cat => cat.id === id);
  }
}

شرح المثال:

  • @Injectable(): واصم يخبر NestJS أن هذا الصنف هو مزود خدمة يمكن حقنه
  • export class CatsService: تعريف صنف الخدمة
  • private readonly cats: Cat[] = []: تخزين مصفوفة من القطط
  • create(cat: Cat): دالة لإنشاء قط جديد
  • findAll(): Cat[]: دالة للحصول على جميع القطط
  • findOne(id: string): Cat: دالة للحصول على قط محدد بواسطة المعرف

تسجيل مزود الخدمة

لكي يعرف NestJS عن مزود الخدمة، يجب تسجيله في وحدة:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],  // تسجيل مزود الخدمة
})
export class CatsModule {}

هناك طرق مختلفة لتسجيل مزودي الخدمات:

@Module({
  providers: [
    // الطريقة القياسية (اختصار للطريقة التالية)
    CatsService,
    
    // الطريقة الصريحة
    {
      provide: CatsService,
      useClass: CatsService,
    },
    
    // استخدام رمز مخصص
    {
      provide: 'CATS_SERVICE',
      useClass: CatsService,
    },
    
    // استخدام قيمة
    {
      provide: 'API_KEY',
      useValue: 'my-api-key',
    },
    
    // استخدام مصنع
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: () => {
        // منطق معقد لإنشاء اتصال بقاعدة البيانات
        return dbConnection;
      },
    },
    
    // مصنع مع تبعيات
    {
      provide: 'CONFIG_SERVICE',
      useFactory: (userService: UsersService) => {
        // منطق يعتمد على UsersService
        return new ConfigService(userService);
      },
      inject: [UsersService],  // تبعيات المصنع
    },
  ],
})
export class AppModule {}

شرح طرق تسجيل مزودي الخدمات:

  • CatsService: الطريقة المختصرة (يتم تسجيل الصنف نفسه كمزود)
  • provide: CatsService, useClass: CatsService: التعريف الصريح
  • provide: 'CATS_SERVICE', useClass: CatsService: استخدام رمز نصي (مفيد للحقن بواسطة @Inject())
  • provide: 'API_KEY', useValue: 'my-api-key': توفير قيمة ثابتة
  • useFactory: () => { ... }: استخدام دالة مصنع لإنشاء مزود ديناميكيًا
  • inject: [UsersService]: تحديد تبعيات دالة المصنع

استخدام مزود الخدمة

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

// حقن باستخدام المنشئ (constructor)
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  findAll(): Cat[] {
    return this.catsService.findAll();
  }
}

// حقن باستخدام @Inject() (للرموز النصية)
import { Controller, Get, Inject } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(@Inject('CATS_SERVICE') private readonly catsService) {}

  @Get()
  findAll(): Cat[] {
    return this.catsService.findAll();
  }
}

شرح طرق حقن مزودي الخدمات:

  • الطريقة الأولى تستخدم حقن المنشئ التقليدي مع نوع TypeScript
  • الطريقة الثانية تستخدم واصم @Inject() مع رمز نصي (مفيد عند استخدام رموز مخصصة لمزودي الخدمات)

7. الخدمات (Services)

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

إنشاء خدمة مع معاملات متعددة

مثال لخدمة تتعامل مع المستخدمين:

import { Injectable, NotFoundException } from '@nestjs/common';
import { User } from './interfaces/user.interface';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UsersService {
  private users: User[] = [];

  create(createUserDto: CreateUserDto): User {
    const id = Date.now().toString(); // إنشاء معرف بسيط
    const newUser: User = {
      id,
      ...createUserDto,
      createdAt: new Date(),
    };
    
    this.users.push(newUser);
    return newUser;
  }

  findAll(): User[] {
    return this.users;
  }

  findOne(id: string): User {
    const user = this.users.find(user => user.id === id);
    
    if (!user) {
      throw new NotFoundException(`المستخدم بالمعرف ${id} غير موجود`);
    }
    
    return user;
  }

  update(id: string, updateUserDto: UpdateUserDto): User {
    const userIndex = this.users.findIndex(user => user.id === id);
    
    if (userIndex === -1) {
      throw new NotFoundException(`المستخدم بالمعرف ${id} غير موجود`);
    }
    
    const updatedUser = {
      ...this.users[userIndex],
      ...updateUserDto,
      updatedAt: new Date(),
    };
    
    this.users[userIndex] = updatedUser;
    return updatedUser;
  }

  remove(id: string): void {
    const userIndex = this.users.findIndex(user => user.id === id);
    
    if (userIndex === -1) {
      throw new NotFoundException(`المستخدم بالمعرف ${id} غير موجود`);
    }
    
    this.users.splice(userIndex, 1);
  }
}

شرح مثال الخدمة:

  • @Injectable(): واصم يجعل الخدمة قابلة للحقن
  • private users: User[] = []: تخزين مؤقت للمستخدمين (في التطبيق الحقيقي، سيتم استخدام قاعدة بيانات)
  • create(): دالة لإنشاء مستخدم جديد
  • findAll(): دالة للحصول على جميع المستخدمين
  • findOne(): دالة للحصول على مستخدم محدد، مع رمي استثناء إذا لم يتم العثور عليه
  • update(): دالة لتحديث مستخدم محدد
  • remove(): دالة لحذف مستخدم محدد

حقن الخدمات في خدمات أخرى

يمكن حقن الخدمات في خدمات أخرى، مما يسمح بتصميم منطق أعمال معقد ومنظم:

import { Injectable } from '@nestjs/common';
import { LoggerService } from '../logger/logger.service';
import { UsersRepository } from './users.repository';

@Injectable()
export class UsersService {
  constructor(
    private readonly logger: LoggerService,
    private readonly usersRepository: UsersRepository,
  ) {}

  async findById(id: string) {
    this.logger.log(`البحث عن المستخدم بالمعرف: ${id}`);
    const user = await this.usersRepository.findById(id);
    
    if (!user) {
      this.logger.warn(`لم يتم العثور على المستخدم بالمعرف: ${id}`);
      return null;
    }
    
    return user;
  }
}

شرح المثال:

  • يتم حقن LoggerService و UsersRepository في UsersService
  • هذا يسمح لـ UsersService باستخدام وظائف كلا الخدمتين
  • الخدمة تستخدم خدمة السجلات (Logger) لتسجيل المعلومات والتحذيرات
  • تستخدم أيضًا مستودع المستخدمين (Repository) للتفاعل مع مصدر البيانات

8. حقن التبعية (Dependency Injection)

حقن التبعية (DI) هو نمط تصميم يتم فيه توفير التبعيات (المكونات الأخرى التي يعتمد عليها المكون) بشكل خارجي بدلاً من إنشائها داخليًا. NestJS يستخدم هذا النمط بشكل أساسي لإدارة العلاقات بين الكائنات.

كيفية عمل حقن التبعية في NestJS

يعتمد NestJS على ثلاثة مفاهيم رئيسية لحقن التبعية:

  • مزودو الخدمات (Providers): كائنات يمكن حقنها في مكونات أخرى كتبعيات
  • المستهلكون (Consumers): المكونات التي تستهلك الخدمات (مثل المتحكمات)
  • حاوية الحقن (IoC Container): نظام الـ NestJS الذي يدير ويحقن التبعيات

مثال عملي

فيما يلي مثال كامل يوضح كيفية عمل حقن التبعية:

// cats.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  private cats = [];
  
  findAll() {
    return this.cats;
  }
  
  create(cat) {
    this.cats.push(cat);
    return cat;
  }
}

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  // حقن CatsService من خلال المنشئ (constructor)
  constructor(private readonly catsService: CatsService) {}
  
  @Get()
  findAll() {
    // استخدام الخدمة المحقونة
    return this.catsService.findAll();
  }
  
  @Post()
  create(@Body() cat) {
    return this.catsService.create(cat);
  }
}

// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService], // تسجيل الخدمة كمزود
})
export class CatsModule {}

شرح مفصل لعملية حقن التبعية:

  1. نُعرّف CatsService ونزوده بواصم @Injectable() ليصبح قابلاً للحقن
  2. في CatsController، نطلب حقن CatsService من خلال المنشئ
  3. نقوم بتسجيل CatsService في مصفوفة providers للوحدة
  4. عندما يقوم NestJS بإنشاء مثيل من CatsController، يكتشف أنه يحتاج إلى CatsService
  5. يقوم NestJS بالبحث عن مزود الخدمة في الوحدة أو الوحدات المستوردة
  6. ينشئ NestJS مثيلاً من CatsService (أو يعيد استخدام مثيل موجود في حالة وجوده)
  7. يمرر NestJS مثيل CatsService إلى منشئ CatsController
  8. الآن يمكن لـ CatsController استخدام CatsService من خلال الخاصية catsService

أنواع مزودي الخدمات

NestJS يدعم عدة أنواع من مزودي الخدمات:

@Module({
  providers: [
    // قيمة - استخدام صنف
    CatsService,
    
    // توفير رمز ل useClass
    {
      provide: CatsService,
      useClass: CatsService,
    },
    
    // توفير واجهة أو رمز نصي
    {
      provide: 'CATS_SERVICE',
      useClass: CatsService,
    },
    
    // توفير قيمة ثابتة
    {
      provide: 'API_KEY',
      useValue: 'secret_key_12345',
    },
    
    // توفير قيمة عبر مصنع (Factory)
    {
      provide: 'CONNECTION',
      useFactory: () => {
        return createConnection();
      },
    },
    
    // مصنع مع حقن تبعيات
    {
      provide: 'USERS_REPOSITORY',
      useFactory: (connection: Connection) => {
        return connection.getRepository(User);
      },
      inject: ['CONNECTION'], // مزودو الخدمات المطلوبين للمصنع
    },
    
    // توفير قيمة عبر خدمة أخرى موجودة
    {
      provide: 'CACHED_CATS',
      useExisting: CatsService,
    },
  ],
})
export class AppModule {}

ملاحظة: حقن التبعية هو نمط تصميم قوي يساعد في جعل الكود أكثر قابلية للاختبار وأكثر مرونة. في NestJS، يتم التعامل مع معظم تفاصيل حقن التبعية تلقائيًا، مما يسمح لك بالتركيز على منطق الأعمال بدلاً من إدارة التبعيات يدويًا.

9. البرمجيات الوسيطة (Middleware)

البرمجيات الوسيطة (Middleware) هي وظائف يتم تنفيذها قبل معالج المسار. لديها حق الوصول إلى كائنات الطلب (request) والاستجابة (response) ودالة next() في دورة طلب واستجابة الخادم.

إنشاء برمجية وسيطة

يمكن إنشاء برمجية وسيطة بتنفيذ واجهة NestMiddleware:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`طلب ${req.method} على المسار: ${req.url}`);
    console.log(`موجه من: ${req.ip}`);
    
    // هام: يجب استدعاء next() لمتابعة سلسلة المعالجة
    next();
  }
}

شرح البرمجية الوسيطة:

  • implements NestMiddleware: تنفيذ واجهة البرمجية الوسيطة
  • use(req, res, next): الطريقة المطلوبة لتنفيذ البرمجية الوسيطة
  • req: كائن الطلب من Express.js
  • res: كائن الاستجابة من Express.js
  • next(): دالة تمرر التحكم إلى البرمجية الوسيطة أو المعالج التالي

برمجية وسيطة وظيفية

يمكن أيضًا إنشاء برمجية وسيطة كدالة بسيطة:

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`طلب ${req.method} على المسار: ${req.url}`);
  next();
}

تطبيق البرمجية الوسيطة

لتطبيق البرمجية الوسيطة، يجب تنفيذ واجهة NestModule في الوحدة:

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats'); // تطبيق على جميع مسارات /cats
  }
}

شرح تطبيق البرمجية الوسيطة:

  • implements NestModule: تنفيذ واجهة لتكوين البرمجيات الوسيطة
  • configure(consumer): الطريقة المطلوبة لتكوين البرمجيات الوسيطة
  • consumer.apply(): تحديد البرمجية الوسيطة المراد تطبيقها
  • forRoutes('cats'): تحديد المسارات التي سيتم تطبيق البرمجية الوسيطة عليها

تطبيق أكثر تحديدًا

يمكن تطبيق البرمجية الوسيطة بشكل أكثر تحديدًا على مسارات وطرق HTTP معينة:

import { RequestMethod } from '@nestjs/common';

configure(consumer: MiddlewareConsumer) {
  consumer
    .apply(LoggerMiddleware)
    .forRoutes(
      { path: 'cats', method: RequestMethod.GET },
      { path: 'cats/:id', method: RequestMethod.DELETE }
    );
}

استبعاد مسارات

يمكن استبعاد مسارات معينة من تطبيق البرمجية الوسيطة:

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.POST },
    { path: 'cats/:id', method: RequestMethod.PATCH }
  )
  .forRoutes('cats');

تطبيق برمجيات وسيطة متعددة

يمكن تطبيق عدة برمجيات وسيطة معًا:

consumer
  .apply(cors(), helmet(), LoggerMiddleware)
  .forRoutes('cats');

مهم: يتم تنفيذ البرمجيات الوسيطة بالترتيب الذي تم تعريفها به في دالة apply().

برمجيات وسيطة عالمية

لتطبيق برمجية وسيطة على جميع المسارات في التطبيق، يمكن استخدام طريقة use() في مثيل التطبيق:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './common/middleware/logger.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(logger); // تطبيق البرمجية الوسيطة على جميع المسارات
  await app.listen(3000);
}
bootstrap();

10. مرشحات الاستثناءات (Exception Filters)

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

الاستثناءات المدمجة

توفر NestJS عدة استثناءات مدمجة تغطي سيناريوهات HTTP الشائعة:

import {
  BadRequestException,
  UnauthorizedException,
  NotFoundException,
  ForbiddenException,
  InternalServerErrorException
} from '@nestjs/common';

// أمثلة على استخدام الاستثناءات المدمجة
throw new BadRequestException('بيانات غير صالحة');
throw new UnauthorizedException('غير مصرح به');
throw new NotFoundException('المورد غير موجود');
throw new ForbiddenException('محظور الوصول');
throw new InternalServerErrorException('خطأ في الخادم');

إنشاء مرشح استثناءات مخصص

لإنشاء مرشح استثناءات مخصص، يجب تنفيذ واجهة ExceptionFilter:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();
    const message = exception.message || 'حدث خطأ';
    
    // تنسيق مخصص للاستجابة
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: message,
    });
  }
}

شرح مرشح الاستثناءات:

  • @Catch(HttpException): واصم يخبر NestJS أن هذا المرشح يجب أن يعالج استثناءات من نوع HttpException
  • implements ExceptionFilter: تنفيذ واجهة مرشح الاستثناءات
  • catch(exception, host): دالة تعالج الاستثناء
  • host.switchToHttp(): الحصول على سياق HTTP
  • ctx.getResponse(): الحصول على كائن الاستجابة
  • ctx.getRequest(): الحصول على كائن الطلب
  • exception.getStatus(): الحصول على رمز حالة HTTP من الاستثناء
  • response.status().json(): إرسال استجابة JSON مخصصة

تطبيق مرشح الاستثناءات

يمكن تطبيق مرشح الاستثناءات بعدة طرق:

1. على مستوى المعالج

import { Controller, Get, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';

@Controller('cats')
export class CatsController {
  @Get()
  @UseFilters(HttpExceptionFilter)
  findAll() {
    throw new Error('طريقة غير مكتملة');
  }
}

2. على مستوى المتحكم

import { Controller, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';

@Controller('cats')
@UseFilters(HttpExceptionFilter)
export class CatsController {
  // جميع معالجات المسارات في هذا المتحكم ستستخدم المرشح
}

3. على مستوى التطبيق

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

التقاط عدة أنواع من الاستثناءات

يمكن للمرشح التقاط عدة أنواع من الاستثناءات:

@Catch(HttpException, BadRequestException, NotFoundException)
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // معالجة الاستثناء
  }
}

التقاط جميع الاستثناءات

لالتقاط جميع الاستثناءات، استخدم @Catch() بدون وسيطات:

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    
    // التحقق من نوع الاستثناء
    let status = 500;
    let message = 'حدث خطأ داخلي في الخادم';
    
    if (exception instanceof HttpException) {
      status = exception.getStatus();
      message = exception.message;
    }
    
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: message,
    });
  }
}

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

11. الأنابيب (Pipes)

الأنابيب (Pipes) في NestJS هي فئات تعالج البيانات المنقولة بين العميل والمعالجات. لها وظيفتان رئيسيتان: التحويل (transformation) والتحقق (validation).

الأنابيب المدمجة

توفر NestJS عدة أنابيب مدمجة:

  • ValidationPipe: للتحقق من صحة البيانات المستلمة
  • ParseIntPipe: لتحويل القيمة إلى عدد صحيح
  • ParseFloatPipe: لتحويل القيمة إلى عدد عشري
  • ParseBoolPipe: لتحويل القيمة إلى قيمة منطقية
  • ParseArrayPipe: لتحويل القيمة إلى مصفوفة
  • ParseUUIDPipe: للتحقق من أن القيمة هي UUID صالح
  • DefaultValuePipe: لتوفير قيمة افتراضية

استخدام الأنابيب المدمجة

مثال على استخدام الأنابيب المدمجة:

import { Controller, Get, Param, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    // id سيكون عدد صحيح، وإلا يتم رمي استثناء BadRequestException
    return `هذا القط رقم ${id}`;
  }
  
  @Get()
  findAll(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number
  ) {
    // استخدام DefaultValuePipe لتوفير قيم افتراضية
    return `عرض القطط، الصفحة ${page} بحد ${limit} قطة`;
  }
}

شرح استخدام الأنابيب:

  • @Param('id', ParseIntPipe): تطبيق ParseIntPipe على معلمة المسار id
  • @Query('page', new DefaultValuePipe(1), ParseIntPipe): إذا كانت معلمة page غير موجودة، استخدم القيمة 1 ثم قم بتحويلها إلى عدد صحيح
  • يتم تطبيق الأنابيب من اليسار إلى اليمين، لذلك DefaultValuePipe يأتي أولاً ثم ParseIntPipe

إنشاء أنبوب مخصص

يمكن إنشاء أنبوب مخصص بتنفيذ واجهة PipeTransform:

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class PositiveIntPipe implements PipeTransform {
  transform(value: string, metadata: ArgumentMetadata): number {
    // تحويل القيمة إلى عدد
    const val = parseInt(value, 10);
    
    // التحقق من أنها عدد صحيح موجب
    if (isNaN(val) || val <= 0) {
      throw new BadRequestException('يجب أن تكون القيمة عددًا صحيحًا موجبًا');
    }
    
    return val;
  }
}
  • implements PipeTransform: تنفيذ واجهة PipeTransform مع تحديد الأنواع
  • transform(value, metadata): الطريقة الإلزامية التي تتلقى القيمة ومعلومات وصفية
  • parseInt(value, 10): تحويل القيمة إلى عدد صحيح
  • isNaN(val) || val <= 0: التحقق من أن القيمة عدد صحيح موجب
  • throw new BadRequestException(): رمي استثناء في حالة فشل التحقق
  • return val: إرجاع القيمة المحولة

استخدام الأنبوب المخصص

يمكن استخدام الأنبوب المخصص بنفس طريقة الأنابيب المدمجة:

@Get(':id')
findOne(@Param('id', PositiveIntPipe) id: number) {
  return this.catsService.findOne(id);
}

أنابيب التحقق من البيانات

يُفضل استخدام المكتبات المتخصصة للتحقق من البيانات المعقدة. NestJS يوفر تكاملًا مع class-validator و class-transformer:

npm install --save class-validator class-transformer

أولًا، نقوم بتعريف DTO (Data Transfer Object) مع واصمات التحقق:

import { IsString, IsInt, IsEmail, Min, Max, Length } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @Length(2, 20)
  readonly name: string;

  @IsInt()
  @Min(18)
  @Max(100)
  readonly age: number;

  @IsEmail()
  readonly email: string;
}

ثم نقوم بتنشيط ValidationPipe في التطبيق:

// لتمكين التحقق على مستوى التطبيق
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

الآن يمكننا استخدام الـ DTO في المتحكم:

@Post()
create(@Body() createUserDto: CreateUserDto) {
  // سيتم التحقق من البيانات تلقائيًا
  // إذا كان هناك خطأ في التحقق، سيتم رمي استثناء BadRequestException
  return this.usersService.create(createUserDto);
}

خيارات ValidationPipe

يمكن تكوين ValidationPipe بخيارات متنوعة:

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true, // إزالة الخصائص غير المحددة في الـ DTO
    forbidNonWhitelisted: true, // رمي استثناء إذا كانت هناك خصائص غير محددة
    transform: true, // تحويل البيانات الواردة تلقائيًا إلى النوع المحدد
    transformOptions: {
      enableImplicitConversion: true, // تحويل الأنواع تلقائيًا
    },
    disableErrorMessages: false, // تعطيل رسائل الخطأ في الإنتاج
    validationError: {
      target: false, // عدم إضافة الكائن المستهدف في رسالة الخطأ
      value: true, // إضافة قيمة الخاصية غير الصالحة
    },
  }),
);

نصيحة: استخدم ValidationPipe على مستوى التطبيق للتحقق من جميع بيانات الإدخال تلقائيًا. هذا يساعد في الحفاظ على أمان وموثوقية التطبيق.

12. الحراس (Guards)

الحراس (Guards) هي فئات مسؤولة عن تحديد ما إذا كان يجب معالجة الطلب من قبل معالج المسار أم لا. تُستخدم غالبًا للمصادقة والتفويض وحماية المسارات.

إنشاء حارس

يتم إنشاء حارس بتنفيذ واجهة CanActivate:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise | Observable {
    const request = context.switchToHttp().getRequest();
    
    // التحقق من وجود رمز مميز (token) في الطلب
    const token = request.headers.authorization;
    
    // تنفيذ منطق المصادقة الخاص بك هنا
    // إرجاع true للسماح بمعالجة الطلب، false لرفض الطلب
    
    return validateToken(token);
  }
  
  // دالة وهمية للتحقق من الرمز المميز
  private validateToken(token: string): boolean {
    // تحقق من الرمز المميز
    // في التطبيق الحقيقي، قد تتحقق من التوقيع، انتهاء الصلاحية، إلخ
    return !!token && token.startsWith('Bearer ');
  }
}

شرح الحارس:

  • implements CanActivate: تنفيذ واجهة الحارس
  • canActivate(context): الطريقة الإلزامية التي تحدد ما إذا كان يجب معالجة الطلب
  • context.switchToHttp().getRequest(): الحصول على كائن الطلب
  • request.headers.authorization: الوصول إلى ترويسة Authorization
  • return boolean: إرجاع true إذا كان يجب معالجة الطلب، false لرفضه

تطبيق الحارس

يمكن تطبيق الحراس بعدة طرق:

1. على مستوى المعالج

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller('cats')
export class CatsController {
  @Get()
  @UseGuards(AuthGuard)
  findAll() {
    return 'هذا المسار محمي بواسطة AuthGuard';
  }
}

2. على مستوى المتحكم

import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller('cats')
@UseGuards(AuthGuard)
export class CatsController {
  // جميع معالجات المسارات في هذا المتحكم ستكون محمية
}

3. على مستوى التطبيق

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './auth.guard';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalGuards(new AuthGuard());
  await app.listen(3000);
}
bootstrap();

حارس مع تبعيات

إذا كان الحارس يحتاج إلى حقن تبعيات (مثل خدمة)، يجب تسجيله كمزود:

// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly authService: AuthService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization;
    
    return this.authService.validateToken(token);
  }
}

// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';

@Module({
  providers: [
    AuthService,
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AppModule {}

إضافة بيانات وصفية للحراس

يمكن استخدام البيانات الوصفية (metadata) مع الحراس لتوفير معلومات إضافية:

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // الحصول على الأدوار المطلوبة من البيانات الوصفية
    const requiredRoles = this.reflector.get('roles', context.getHandler());
    
    // إذا لم يتم تحديد أدوار، فالسماح بالوصول
    if (!requiredRoles) {
      return true;
    }
    
    const request = context.switchToHttp().getRequest();
    const user = request.user; // يفترض أن المستخدم تم إعداده مسبقًا
    
    // التحقق مما إذا كان المستخدم لديه أي من الأدوار المطلوبة
    return user && user.roles && requiredRoles.some(role => user.roles.includes(role));
  }
}

// استخدام واصم الأدوار مع الحارس
@Controller('cats')
export class CatsController {
  @Get()
  @Roles('admin', 'editor')
  @UseGuards(RolesGuard)
  findAll() {
    return 'هذا المسار يتطلب دور admin أو editor';
  }
}

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

13. المعترضات (Interceptors)

المعترضات (Interceptors) هي فئات تعترض تنفيذ معالجات المسارات قبل وبعد معالجة الطلب. يمكن استخدامها لعدة أغراض مثل:

  • إضافة منطق قبل أو بعد تنفيذ المعالج
  • تحويل نتيجة المعالج
  • تحويل الاستثناء المرمي من المعالج
  • توسيع سلوك المعالج
  • تجاوز المعالج تمامًا وفقًا لشروط معينة

إنشاء معترض

يتم إنشاء معترض بتنفيذ واجهة NestInterceptor:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable {
    console.log('قبل...');
    
    const now = Date.now();
    
    return next
      .handle()
      .pipe(
        tap(() => console.log(`بعد... استغرقت ${Date.now() - now}ms`)),
      );
  }
}

شرح المعترض:

  • implements NestInterceptor: تنفيذ واجهة المعترض
  • intercept(context, next): الطريقة الإلزامية التي تنفذ قبل وبعد معالجة الطلب
  • console.log('قبل...'): تنفيذ رمز قبل معالجة الطلب
  • next.handle(): تمرير التنفيذ إلى المعالج
  • .pipe(tap(...)): تنفيذ رمز بعد معالجة الطلب

تطبيق المعترض

يمكن تطبيق المعترضات بعدة طرق:

1. على مستوى المعالج

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('cats')
export class CatsController {
  @Get()
  @UseInterceptors(LoggingInterceptor)
  findAll() {
    return 'هذا المسار يستخدم LoggingInterceptor';
  }
}

2. على مستوى المتحكم

import { Controller, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';

@Controller('cats')
@UseInterceptors(LoggingInterceptor)
export class CatsController {
  // جميع معالجات المسارات في هذا المتحكم ستستخدم المعترض
}

3. على مستوى التطبيق

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new LoggingInterceptor());
  await app.listen(3000);
}
bootstrap();

تحويل الاستجابة

يمكن استخدام المعترضات لتحويل نتيجة المعالج:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response {
  data: T;
  timestamp: string;
  status: number;
}

@Injectable()
export class TransformInterceptor implements NestInterceptor> {
  intercept(context: ExecutionContext, next: CallHandler): Observable> {
    // الحصول على كائن الاستجابة لمعرفة رمز الحالة
    const response = context.switchToHttp().getResponse();
    
    return next.handle().pipe(
      map(data => ({
        data,
        timestamp: new Date().toISOString(),
        status: response.statusCode,
      })),
    );
  }
}

شرح معترض التحويل:

  • interface Response: تعريف واجهة للاستجابة المحولة
  • implements NestInterceptor>: تحديد نوع المدخلات والمخرجات
  • map(data => ({ ... })): تحويل البيانات إلى الشكل المطلوب

معالجة الاستثناءات

يمكن استخدام المعترضات لمعالجة الاستثناءات في سلسلة المعالجة:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, BadGatewayException } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable {
    return next
      .handle()
      .pipe(
        catchError(err => throwError(() => new BadGatewayException('حدث خطأ أثناء الاتصال بالخدمة الخارجية'))),
      );
  }
}

تجاوز المعالج

يمكن تجاوز المعالج الأصلي تمامًا بواسطة المعترض:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  private readonly cache = new Map();

  intercept(context: ExecutionContext, next: CallHandler): Observable {
    const request = context.switchToHttp().getRequest();
    const cacheKey = request.url;
    
    // التحقق من وجود استجابة مخزنة مؤقتًا
    if (this.cache.has(cacheKey)) {
      console.log(`إرجاع استجابة مخزنة مؤقتًا لـ ${cacheKey}`);
      return of(this.cache.get(cacheKey));
    }
    
    // معالجة الطلب وتخزين النتيجة
    return next.handle().pipe(
      tap(response => {
        console.log(`تخزين الاستجابة لـ ${cacheKey}`);
        this.cache.set(cacheKey, response);
      }),
    );
  }
}

نصيحة: المعترضات هي أداة قوية لإضافة وظائف متعددة مثل التسجيل، التخزين المؤقت، تحويل المخرجات، وإدارة الاستثناءات. استخدمها لفصل هذه المخاوف عن منطق الأعمال الأساسي.

14. العمل مع قواعد البيانات

NestJS يوفر تكاملاً سلسًا مع العديد من أطر قواعد البيانات والـ ORMs. سنستكشف الطرق المختلفة للعمل مع قواعد البيانات في تطبيقات NestJS.

خيارات قواعد البيانات

هناك عدة خيارات للعمل مع قواعد البيانات في NestJS:

1. TypeORM

إطار ORM شائع لـ TypeScript و JavaScript، يدعم العديد من قواعد البيانات SQL مثل MySQL و PostgreSQL و SQLite.

2. Mongoose

مكتبة نمذجة البيانات لـ MongoDB، تُستخدم للعمل مع قواعد بيانات MongoDB.

3. Prisma

ORM حديث مع معالج استعلام آمن وتوليد تلقائي للنوع، يدعم PostgreSQL و MySQL و SQLite و SQL Server.

4. Sequelize

ORM مبني على الوعود لـ Node.js، يدعم PostgreSQL و MySQL و MariaDB و SQLite و MS SQL Server.

سنركز على TypeORM و Mongoose كونهما الأكثر استخدامًا مع NestJS.