التوثيق الشامل لإطار عمل 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 باستخدام AppModuleawait app.listen(3000);
: تشغيل التطبيق على المنفذ 3000bootstrap();
: استدعاء الدالة لبدء التطبيق
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) Moduleimport { AppController } from './app.controller';
: استيراد المتحكم الرئيسيimport { AppService } from './app.service';
: استيراد الخدمة الرئيسية@Module({ ... })
: واصم لتعريف وحدة في NestJSimports: []
: قائمة الوحدات المستوردة (فارغة حاليًا)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()
: واصم لتعريف معالج طلب GETgetHello(): 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()
: واصم لتعريف مزود خدمة قابل للحقن في NestJSexport 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 على المسار /usersfindAll(): User[]
: دالة للحصول على جميع المستخدمين@Get(':id')
: يستجيب لطلبات GET مع معلمة id (مثل /users/123)findOne(@Param('id') id: string)
: دالة تستخدم الواصم Param لاستخراج معلمة المسار@Post()
: يستجيب لطلبات POST على المسار /userscreate(@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 {}
شرح مفصل لعملية حقن التبعية:
- نُعرّف
CatsService
ونزوده بواصم@Injectable()
ليصبح قابلاً للحقن - في
CatsController
، نطلب حقنCatsService
من خلال المنشئ - نقوم بتسجيل
CatsService
في مصفوفةproviders
للوحدة - عندما يقوم NestJS بإنشاء مثيل من
CatsController
، يكتشف أنه يحتاج إلىCatsService
- يقوم NestJS بالبحث عن مزود الخدمة في الوحدة أو الوحدات المستوردة
- ينشئ NestJS مثيلاً من
CatsService
(أو يعيد استخدام مثيل موجود في حالة وجوده) - يمرر NestJS مثيل
CatsService
إلى منشئCatsController
- الآن يمكن لـ
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.jsres
: كائن الاستجابة من Express.jsnext()
: دالة تمرر التحكم إلى البرمجية الوسيطة أو المعالج التالي
برمجية وسيطة وظيفية
يمكن أيضًا إنشاء برمجية وسيطة كدالة بسيطة:
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 أن هذا المرشح يجب أن يعالج استثناءات من نوع HttpExceptionimplements ExceptionFilter
: تنفيذ واجهة مرشح الاستثناءاتcatch(exception, host)
: دالة تعالج الاستثناءhost.switchToHttp()
: الحصول على سياق HTTPctx.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
: الوصول إلى ترويسة Authorizationreturn 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.