بناء واجهات برمجة تطبيقات احترافية: دليل شامل لـ NestJS و PostgreSQL و Sequelize
NestJS إطار عمل قويًا يعتمد على بنية MVC (Model-View-Controller) لتطوير تطبيقات الخادم (server-side) الفعالة والقابلة للتوسع باستخدام Node.js. يتميز بدعمه الكامل للغة TypeScript، مع إمكانية الكتابة بلغة JavaScript النقية أيضًا. يجمع NestJS ببراعة بين مفاهيم البرمجة الشيئية (Object Oriented Programming)، والبرمجة الوظيفية (Functional Programming)، والبرمجة التفاعلية الوظيفية (Functional Reactive Programming). من أبرز مزاياه توفيره لهيكلية تطبيق جاهزة، مما يمكّن المطورين والفرق من بناء تطبيقات سهلة الاختبار، قابلة للتوسع، ذات ترابط ضعيف (loosely coupled)، وسهلة الصيانة.
ماذا سنبني في هذا الدليل؟
في هذا الدليل، سنخوض رحلة شيقة في عالم NestJS، حيث سنقوم ببناء تطبيق مدونة مصغرة (Mini Blog) يعمل كواجهة برمجة تطبيقات ويب من نوع RESTful API. سيغطي هذا التطبيق البسيط الجوانب الأساسية التالية:
- إعداد قاعدة البيانات باستخدام
SequelizeوPostgreSQL. - نظام المصادقة (
Authentication) بواسطةPassport(تسجيل الدخول والتسجيل). - التحقق من صحة مدخلات المستخدم (
Validating user input). - حماية المسارات (
Route protection) باستخدامJWT(JSON Web Tokens). - عمليات إنشاء، قراءة، تحديث، وحذف منشورات المدونة (
CRUD operations).
المتطلبات الأساسية
لتحقيق أقصى استفادة من هذا الدليل، يُعد الإلمام بلغة TypeScript و JavaScript أمرًا بالغ الأهمية. الخبرة المسبقة مع Angular تُعد ميزة إضافية، ولكن لا داعي للقلق؛ فالمقال سيشرح جميع المفاهيم الضرورية المتعلقة بـ NestJS. ستحتاج إلى تثبيت Postman لاختبار نقاط نهاية الواجهة البرمجية (API endpoints) التي سنبنيها. تأكد أيضًا من تثبيت Node.js (الإصدار 8.9.0 أو أحدث) على جهازك. أخيرًا، يمكنك العثور على رابط المستودع النهائي للمشروع على GitHub.
المكونات الأساسية لـ NestJS
قبل الشروع في البناء، سنتناول بعض المفاهيم والتجريدات الأساسية التي ستساعدك على فهم مكان وضع منطق العمل الخاص بك في كل مشروع. يتشابه NestJS كثيرًا مع Angular، لذا إذا كنت ملمًا بمفاهيم Angular، ستجد الأمر سهلًا ومباشرًا. ومع ذلك، سأفترض أنك لا تملك معرفة مسبقة بهذه المفاهيم وسأشرحها لك بالتفصيل.
المتحكمات (Controllers)
المتحكم (Controller) هو المسؤول عن الاستماع للطلبات الواردة إلى تطبيقك ومن ثم صياغة الاستجابات التي يتم إرسالها. على سبيل المثال، عند إجراء استدعاء API إلى المسار /posts، سيتولى المتحكم معالجة هذا الطلب وإرجاع الاستجابة المناسبة التي حددتها.
import { Controller, Get } from '@nestjs/common' ;
@Controller ( 'posts' )
export class PostsController {
@Get ()
findAll(): string {
return 'This action returns all posts' ;
}
@Get (':id')
findOne( @Param ( 'id' ) id: number ): string {
return 'This action returns one post' ;
}
}
هذا مجرد تعريف بسيط لفئة (Class) في TypeScript أو JavaScript مزود بمُزين (decorator) وهو @Controller. يجب أن تحتوي جميع متحكمات NestJS على هذا المُزين، وهو ضروري لتعريف متحكم أساسي. يتيح لك NestJS تحديد مساراتك كمعامل في مُزين @Controller(). يساعد هذا في تجميع مجموعة من المسارات ذات الصلة ويقلل من تكرار التعليمات البرمجية. أي طلب إلى /posts سيتولى هذا المتحكم معالجته. على مستوى توابع الفئة (class methods)، يمكنك تحديد التابع الذي يجب أن يتعامل مع طلبات HTTP من نوع GET، POST، DELETE، أو PUT/PATCH. في مثالنا، التابع findAll() المزود بالمُزين @Get() يتعامل مع جميع طلبات GET HTTP لجلب جميع منشورات المدونة، بينما التابع findOne() المزود بالمُزين @Get(':id') سيتعامل مع طلب GET /posts/1.
المزودون (Providers)
صُممت المزودات (Providers) لتجريد أي شكل من أشكال التعقيد والمنطق إلى فئة منفصلة. يمكن أن يكون المزود خدمة (service)، أو مستودعًا (repository)، أو مصنعًا (factory)، أو مساعدًا (helper). المزودات هي فئات TypeScript/JavaScript عادية تسبق تعريفها مُزين @Injectable(). تمامًا مثل الخدمات في Angular، يمكنك إنشاء المزودات وحقنها (inject) في متحكمات أخرى أو مزودات أخرى. مثال جيد على استخدام مزود الخدمة هو إنشاء PostService الذي يجرد جميع الاتصالات بقاعدة البيانات إلى هذه الخدمة، مما يحافظ على نظافة وبساطة PostsController.
import { Injectable } from '@nestjs/common' ;
import { Post } from './interfaces/post.interface' ;
@Injectable ()
export class PostsService {
private readonly posts: Post[] = [];
create(post: Post) {
this .posts.push(post);
}
findAll(): Post[] {
return this .posts;
}
}
export interface Post {
title: string ;
body: string ;
}
هذه مجرد فئة TypeScript عادية مع مُزين @Injectable() (هذه هي الطريقة التي يعرف بها NestJS أنها مزود). Post هي مجرد واجهة (interface) للتحقق من النوع (type checking). هنا، نستخدم بنية بيانات بسيطة لتخزين البيانات. في مشروع حقيقي، ستتواصل هذه الخدمة مع قاعدة البيانات.
الوحدات (Modules)
الوحدة (Module) هي فئة JavaScript/TypeScript مزودة بمُزين @Module(). يوفر مُزين @Module() بيانات وصفية (metadata) يستخدمها NestJS لتنظيم هيكل التطبيق. تُعد الوحدات جانبًا بالغ الأهمية في NestJS، ويجب أن يوفر كل تطبيق وحدة واحدة على الأقل: الوحدة الجذرية للتطبيق (application root module). الوحدة الجذرية هي نقطة البداية التي يستخدمها NestJS لبناء مخطط التطبيق. يجب تجميع خدمة المنشورات (post service)، والمتحكم (controller)، وكيان المنشور (post entity)، وكل ما يتعلق بالمنشورات في وحدة واحدة (PostsModule). أدناه، قمنا بتعريف PostsModule:
import { Module } from '@nestjs/common' ;
import { PostsController } from './posts.controller' ;
import { PostsService } from './posts.service' ;
@Module ({
controllers: [PostsController],
providers: [PostsService],
})
export class PostsModule {}
بعد ذلك، نقوم باستيراد هذه الوحدة إلى الوحدة الجذرية AppModule:
import { Module } from '@nestjs/common' ;
import { PostsModule } from './posts/posts.module' ;
@Module ({
imports: [PostsModule],
})
export class AppModule {}
يقبل مُزين @Module() كائنًا واحدًا تصف خصائصه الوحدة:
imports:الوحدات الأخرى التي تحتاجها هذه الوحدة.exports:افتراضيًا، تقوم الوحدات بتغليف المزودات. من المستحيل حقن المزودات التي ليست جزءًا مباشرًا من الوحدة الحالية أو التي لم يتم تصديرها من الوحدات المستوردة. لجعل مزودات الوحدة الحالية متاحة للوحدات الأخرى في التطبيق، يجب تصديرها هنا. يمكننا أيضًا تصدير الوحدات التي استوردناها.controllers:مجموعة المتحكمات المعرفة في هذه الوحدة والتي يجب تهيئتها.providers:ببساطة، جميع خدماتنا ومزوداتنا داخل الوحدة ستكون هنا.
المُعترضات (Interceptors)
المُعترض (Interceptor) هو نوع متخصص من البرمجيات الوسيطة (middleware) يتيح لك فحص الطلبات الواردة إلى التطبيق. يمكنك فحص الطلب إما قبل أن يصل إلى المتحكم (controller) أو بعد أن ينتهي المتحكم من معالجة الطلب وقبل أن يصل إلى جانب العميل (client-side) كاستجابة. يمكنك معالجة البيانات وتعديلها أثناء خروجها عبر المُعترض.
الحراس (Guards)
الحارس (Guard) هو أيضًا نوع خاص من البرمجيات الوسيطة يُستخدم بشكل أساسي للمصادقة (authentication) والتفويض (authorization). وظيفته الوحيدة هي إرجاع قيمة منطقية (boolean) إما true أو false. يحدد الحراس ما إذا كان سيتم التعامل مع طلب معين بواسطة معالج المسار (route handler) أم لا، وذلك بناءً على شروط معينة (مثل الأذونات، الأدوار، قوائم التحكم بالوصول ACLs، وما إلى ذلك) الموجودة وقت التشغيل. يجب أن يقوم الحارس أيضًا بتطبيق الواجهة CanActivate.
الأنابيب (Pipes)
الأنابيب (Pipes) هي أيضًا نوع خاص من البرمجيات الوسيطة التي تقع بين العميل والمتحكم. تُستخدم بشكل أساسي للتحقق من صحة البيانات (validation) وتحويلها قبل أن تصل إلى المتحكم.
كائن نقل البيانات (DTO – Data Transfer Object)
كائن نقل البيانات (DTO) هو كائن يحدد كيفية إرسال البيانات عبر الشبكة. تُستخدم هذه الكائنات أيضًا للتحقق من صحة البيانات وتدقيق الأنواع (type checking).
الواجهات (Interfaces)
تُستخدم واجهات TypeScript فقط للتحقق من الأنواع (type-checking) ولا يتم تحويلها إلى شيفرة JavaScript عند الترجمة (compilation).
التثبيت والإعداد الأولي
تثبيت واجهة سطر الأوامر (CLI) لـ NestJS
يأتي NestJS مزودًا بواجهة سطر أوامر (CLI) رائعة تسهل عملية إنشاء هياكل تطبيقات NestJS بكل سهولة. في طرفيتك (terminal) أو موجه الأوامر (cmd)، قم بتشغيل الأمر التالي:
npm i -g @nestjs/cli
الآن، أصبح NestJS CLI مثبتًا عالميًا على جهازك.
إنشاء مشروع NestJS جديد
انتقل في طرفيتك أو موجه الأوامر إلى الدليل الذي ترغب في إنشاء تطبيقك فيه، ثم قم بتشغيل الأوامر التالية:
nest new nest-blog-api
cd nest-blog-api
npm run start:dev
بعد تشغيل الأوامر، انتقل إلى http://localhost:3000 في أي متصفح لديك. يجب أن تشاهد رسالة “Hello World“. تهانينا! لقد أنشأت أول تطبيق NestJS لك. لنواصل.
ملاحظة هامة (تصحيح خطأ TypeScript)
ملاحظة: وقت كتابة هذا الدليل، إذا واجهت خطأ عند تشغيل الأمر npm run start:dev، قم بتغيير إصدار typescript:3.4.2 في ملف package.json الخاص بك إلى typescript:3.7.2. بعد ذلك، احذف مجلد node_modules وملف package-lock.json، ثم أعد تشغيل الأمر npm i.
هيكل المجلدات
يجب أن يبدو هيكل مجلدات مشروعك على النحو التالي:

هيكل مجلدات NestJS.
إعداد Sequelize وقاعدة البيانات
تثبيت الاعتمادات (Dependencies)
سنبدأ بتثبيت الاعتمادات التالية. تأكد من أن طرفيتك أو موجه الأوامر موجود حاليًا في الدليل الجذري لمشروعك. ثم قم بتشغيل الأوامر التالية:
npm install -g sequelize
npm install --save sequelize sequelize-typescript pg-hstore pg
npm install --save-dev @types/sequelize
npm install dotenv --save
إنشاء وحدة قاعدة البيانات
الآن، لنقم بإنشاء وحدة قاعدة بيانات (database module). قم بتشغيل الأمر التالي:
nest generate module /core/database
واجهة قاعدة البيانات (Database Interface)
داخل مجلد database، أنشئ مجلدًا باسم interfaces، ثم أنشئ ملفًا باسم dbConfig.interface.ts بداخله. هذا الملف سيحتوي على واجهة إعدادات قاعدة البيانات. يجب أن تحتوي كل بيئة من بيئات قاعدة البيانات اختياريًا على الخصائص التالية. انسخ والصق التعليمات البرمجية التالية:
export interface IDatabaseConfigAttributes {
username?: string ;
password?: string ;
database?: string ;
host?: string ;
port?: number | string ;
dialect?: string ;
urlDatabase?: string ;
}
export interface IDatabaseConfig {
development: IDatabaseConfigAttributes;
test: IDatabaseConfigAttributes;
production: IDatabaseConfigAttributes;
}
إعدادات قاعدة البيانات (Database Configuration)
الآن، لنقم بإنشاء إعدادات بيئة قاعدة البيانات. داخل مجلد database، أنشئ ملفًا باسم database.config.ts. انسخ والصق التعليمات البرمجية أدناه:
import * as dotenv from 'dotenv' ;
import { IDatabaseConfig } from './interfaces/dbConfig.interface' ;
dotenv.config();
export const databaseConfig: IDatabaseConfig = {
development: {
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME_DEVELOPMENT,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
},
test: {
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME_TEST,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
},
production: {
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME_PRODUCTION,
host: process.env.DB_HOST,
dialect: process.env.DB_DIALECT,
},
};
ستحدد البيئة (environment) أي من هذه الإعدادات يجب استخدامها.
ملف .env
في المجلد الجذري لمشروعك، أنشئ ملفين باسم .env و .env.sample. انسخ والصق التعليمات البرمجية التالية في كلا الملفين:
DB_HOST=localhost
DB_PORT=5432
DB_USER=database_user_name
DB_PASS=database_password
DB_DIALECT=postgres
DB_NAME_TEST=test_database_name
DB_NAME_DEVELOPMENT=development_database_name
DB_NAME_PRODUCTION=production_database_name
JWTKEY=random_secret_key
TOKEN_EXPIRATION=48h
BEARER=Bearer
املأ القيم بالمعلومات الصحيحة – في ملف .env فقط – وتأكد من إضافته إلى ملف .gitignore لتجنب رفعه عبر الإنترنت. ملف .env.sample مخصص لأولئك الذين يرغبون في تنزيل مشروعك واستخدامه، لذا يمكنك رفعه عبر الإنترنت.
نصائح هامة:
- يجب أن يكون اسم المستخدم وكلمة المرور واسم قاعدة البيانات هي نفسها التي استخدمتها لإعداد
PostgreSQL. - أنشئ قاعدة بيانات
PostgreSQLبالاسم الذي حددته.
يوفر NestJS حزمة @nestjs/config جاهزة للمساعدة في تحميل ملف .env الخاص بنا. لاستخدامها، نقوم أولاً بتثبيت الاعتماد المطلوب. قم بتشغيل الأمر التالي:
npm i --save @nestjs/config
استورد @nestjs/config إلى الوحدة الجذرية لتطبيقك (AppModule):
import { Module } from '@nestjs/common' ;
import { ConfigModule } from '@nestjs/config' ;
@Module ({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
]
})
export class AppModule { }
سيؤدي تعيين ConfigModule.forRoot({ isGlobal: true }) إلى جعل خصائص ملف .env متاحة في جميع أنحاء التطبيق.
مزود قاعدة البيانات (Database Provider)
لنقم بإنشاء مزود لقاعدة البيانات. داخل مجلد database، أنشئ ملفًا باسم database.providers.ts. سيحتوي مجلد core على جميع الإعدادات الأساسية، والتكوينات، والوحدات المشتركة، والأنابيب (pipes)، والحراس (guards)، والبرمجيات الوسيطة (middlewares). في ملف database.providers.ts، انسخ والصق هذه التعليمات البرمجية:
import { Sequelize } from 'sequelize-typescript' ;
import { SEQUELIZE, DEVELOPMENT, TEST, PRODUCTION } from '../constants' ;
import { databaseConfig } from './database.config' ;
export const databaseProviders = [{
provide: SEQUELIZE,
useFactory: async () => {
let config;
switch (process.env.NODE_ENV) {
case DEVELOPMENT:
config = databaseConfig.development;
break ;
case TEST:
config = databaseConfig.test;
break ;
case PRODUCTION:
config = databaseConfig.production;
break ;
default :
config = databaseConfig.development;
}
const sequelize = new Sequelize(config);
sequelize.addModels([ 'models goes here' ]);
await sequelize.sync();
return sequelize;
},
}];
هنا، يقرر التطبيق البيئة التي يعمل عليها حاليًا ثم يختار إعدادات البيئة المناسبة. ستتم إضافة جميع نماذجنا (models) إلى الدالة sequelize.addModels([User, Post]). حاليًا، لا توجد نماذج.
أفضل الممارسات:
من الجيد الاحتفاظ بجميع القيم النصية في ملف ثابت (constant file) وتصديرها لتجنب الأخطاء الإملائية في هذه القيم. سيكون لديك أيضًا مكان واحد لتغيير الأشياء.
داخل مجلد core، أنشئ مجلدًا باسم constants، وداخله أنشئ ملف index.ts. الصق التعليمات البرمجية التالية:
export const SEQUELIZE = 'SEQUELIZE' ;
export const DEVELOPMENT = 'development' ;
export const TEST = 'test' ;
export const PRODUCTION = 'production' ;
لنقم الآن بإضافة مزود قاعدة البيانات إلى وحدة قاعدة البيانات الخاصة بنا. انسخ والصق هذه التعليمات البرمجية:
import { Module } from '@nestjs/common' ;
import { databaseProviders } from './database.providers' ;
@Module ({
providers: [...databaseProviders],
exports : [...databaseProviders],
})
export class DatabaseModule { }
لقد قمنا بتصدير مزود قاعدة البيانات exports: [...databaseProviders] لجعله متاحًا لبقية أجزاء التطبيق التي تحتاجه. الآن، لنقم باستيراد وحدة قاعدة البيانات إلى الوحدة الجذرية لتطبيقنا (AppModule) لجعلها متاحة لجميع خدماتنا.
import { Module } from '@nestjs/common' ;
import { ConfigModule } from '@nestjs/config' ;
import { DatabaseModule } from './core/database/database.module' ;
@Module ({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
DatabaseModule,
]
})
export class AppModule { }
تعيين بادئة عالمية لنقاط النهاية (Global Endpoint Prefix)
قد نرغب في أن تبدأ جميع نقاط نهاية الواجهة البرمجية (API endpoints) الخاصة بنا بـ api/v1 لأغراض تحديد الإصدارات المختلفة. لا نريد أن نضطر إلى إضافة هذه البادئة إلى جميع المتحكمات (controllers) الخاصة بنا. لحسن الحظ، يوفر NestJS طريقة لتعيين بادئة عامة. في ملف main.ts، أضف السطر app.setGlobalPrefix('api/v1');:
import { NestFactory } from '@nestjs/core' ;
import { AppModule } from './app.module' ;
import { ValidateInputPipe } from './core/pipes/validate.pipe' ;
async function bootstrap ( ) {
const app = await NestFactory.create(AppModule);
// global endpoints prefix
app.setGlobalPrefix( 'api/v1' );
// handle all user input validation globally
app.useGlobalPipes( new ValidateInputPipe());
await app.listen( 3000 );
}
bootstrap();
وحدة المستخدمين (User Module)
إنشاء وحدة المستخدمين
لنقم بإضافة وحدة المستخدمين (User module) للتعامل مع جميع العمليات المتعلقة بالمستخدمين ولتتبع من يقوم بإنشاء أي منشور. قم بتشغيل الأمر التالي:
nest generate module /modules/users
سيؤدي هذا إلى إضافة هذه الوحدة تلقائيًا إلى وحدتنا الجذرية AppModule.
إنشاء خدمة المستخدمين (User Service)
قم بتشغيل الأمر التالي لإنشاء خدمة المستخدمين:
nest generate service /modules/users
سيؤدي هذا إلى إضافة هذه الخدمة تلقائيًا إلى وحدة المستخدمين (Users module).
إعداد نموذج مخطط قاعدة بيانات المستخدم (User Database Schema Model)
داخل مجلد modules/users، أنشئ ملفًا باسم user.entity.ts ثم انسخ والصق هذه التعليمات البرمجية:
import { Table, Column, Model, DataType } from 'sequelize-typescript' ;
@Table
export class User extends Model<User> {
@Column ({
type : DataType.STRING,
allowNull: false ,
})
name: string ;
@Column ({
type : DataType.STRING,
unique: true ,
allowNull: false ,
})
email: string ;
@Column ({
type : DataType.STRING,
allowNull: false ,
})
password: string ;
@Column ({
type : DataType.ENUM,
values: [ 'male' , 'female' ],
allowNull: false ,
})
gender: string ;
}
هنا، نحدد ما سيحتويه جدول المستخدمين (User table) الخاص بنا. يوفر المُزين @Column() معلومات حول كل عمود في الجدول. سيحتوي جدول المستخدمين على أعمدة name و email و password و gender. لقد قمنا باستيراد جميع مُزينات Sequelize من مكتبة sequelize-typescript.
كائن نقل بيانات المستخدم (User DTO)
لنقم بإنشاء مخطط كائن نقل بيانات المستخدم (User DTO - Data Transfer Object) الخاص بنا. داخل مجلد users، أنشئ مجلدًا باسم dto. ثم أنشئ ملفًا باسم user.dto.ts بداخله. الصق التعليمات البرمجية التالية:
export class UserDto {
readonly name: string ;
readonly email: string ;
readonly password: string ;
readonly gender: string ;
}
مزود مستودع المستخدمين (User Repository Provider)
الآن، لنقم بإنشاء مزود مستودع المستخدمين (User Repository provider). داخل مجلد users، أنشئ ملفًا باسم users.providers.ts. يُستخدم هذا المزود للتواصل مع قاعدة البيانات.
import { User } from './user.entity' ;
import { USER_REPOSITORY } from '../../core/constants' ;
export const usersProviders = [{
provide: USER_REPOSITORY,
useValue: User,
}];
أضف السطر export const USER_REPOSITORY = 'USER_REPOSITORY'; إلى ملف الثوابت index.ts. كما يجب إضافة مزود المستخدم إلى وحدة المستخدمين (User module). لاحظ أننا أضفنا UserService إلى مصفوفة exports لدينا، وذلك لأننا سنحتاجه خارج وحدة المستخدمين.
import { Module } from '@nestjs/common' ;
import { UsersService } from './users.service' ;
import { usersProviders } from './users.providers' ;
@Module ({
providers: [UsersService, ...usersProviders],
exports : [UsersService],
})
export class UsersModule {}
لنقم بتغليف عمليات المستخدم داخل UsersService. انسخ والصق التعليمات البرمجية التالية:
import { Injectable, Inject } from '@nestjs/common' ;
import { User } from './user.entity' ;
import { UserDto } from './dto/user.dto' ;
import { USER_REPOSITORY } from '../../core/constants' ;
@Injectable ()
export class UsersService {
constructor ( @Inject (USER_REPOSITORY) private readonly userRepository: typeof User ) { }
async create(user: UserDto): Promise <User> {
return await this .userRepository.create<User>(user);
}
async findOneByEmail(email: string ): Promise <User> {
return await this .userRepository.findOne<User>({ where: { email } });
}
async findOneById(id: number ): Promise <User> {
return await this .userRepository.findOne<User>({ where: { id } });
}
}
هنا، قمنا بحقن مستودع المستخدمين (user repository) للتواصل مع قاعدة البيانات (DB).
create(user: UserDto):تنشئ هذه الدالة مستخدمًا جديدًا في جدول المستخدمين وتُرجع كائن المستخدم الذي تم إنشاؤه حديثًا.findOneByEmail(email: string):تُستخدم هذه الدالة للبحث عن مستخدم في جدول المستخدمين بواسطة البريد الإلكتروني وتُرجع كائن المستخدم.findOneById(id: number):تُستخدم هذه الدالة للبحث عن مستخدم في جدول المستخدمين بواسطة معرف المستخدم (User Id) وتُرجع كائن المستخدم.
سنستخدم هذه الدوال لاحقًا. أخيرًا، لنقم بإضافة نموذج المستخدم (User model) إلى ملف database.providers.ts ضمن الدالة sequelize.addModels([User]);.
import { Sequelize } from 'sequelize-typescript' ;
import { SEQUELIZE, DEVELOPMENT, TEST, PRODUCTION } from '../constants' ;
import { databaseConfig } from './database.config' ;
import { User } from '../../modules/users/user.entity' ;
export const databaseProviders = [{
provide: SEQUELIZE,
useFactory: async () => {
let config;
switch (process.env.NODE_ENV) {
case DEVELOPMENT:
config = databaseConfig.development;
break ;
case TEST:
config = databaseConfig.test;
break ;
case PRODUCTION:
config = databaseConfig.production;
break ;
default :
config = databaseConfig.development;
}
const sequelize = new Sequelize(config);
sequelize.addModels([User]);
await sequelize.sync();
return sequelize;
},
}];
وحدة المصادقة (Auth Module)
إنشاء وحدة المصادقة
ستتولى هذه الوحدة مسؤولية مصادقة المستخدمين (تسجيل الدخول والتسجيل). قم بتشغيل الأمر التالي:
nest generate module /modules/auth
سيؤدي هذا إلى إضافة هذه الوحدة تلقائيًا إلى وحدتنا الجذرية AppModule.
إنشاء خدمة المصادقة (Auth Service)
قم بتشغيل الأمر التالي لإنشاء خدمة المصادقة:
nest generate service /modules/auth
سيؤدي هذا إلى إضافة هذه الخدمة تلقائيًا إلى وحدة المصادقة (Auth module).
إنشاء متحكم المصادقة (Auth Controller)
قم بتشغيل الأمر التالي لإنشاء متحكم المصادقة:
nest g co /modules/auth
سيؤدي هذا إلى إضافة هذا المتحكم تلقائيًا إلى وحدة المصادقة. ملاحظة: g هو اختصار لـ generate و co هو اختصار لـ controller.
استراتيجيات المصادقة
سنستخدم Passport للتعامل مع المصادقة في تطبيقنا. من السهل دمج هذه المكتبة مع تطبيق NestJS باستخدام وحدة @nestjs/passport. سنقوم بتطبيق استراتيجيتين للمصادقة في هذا التطبيق:
- استراتيجية
Passportالمحلية (Local Passport Strategy): ستُستخدم هذه الاستراتيجية لتسجيل دخول المستخدمين. ستتحقق مما إذا كان البريد الإلكتروني/اسم المستخدم وكلمة المرور المقدمة من المستخدم صالحة أم لا. إذا كانت بيانات اعتماد المستخدم صالحة، فستُرجع رمزًا (token) وكائن مستخدم (user object)، وإلا، فستُلقي استثناءً. - استراتيجية
Passport JWT(JWT Passport Strategy): ستُستخدم هذه الاستراتيجية لحماية الموارد المحمية. فقط المستخدمون المصادق عليهم الذين يملكون رمزًا صالحًا سيتمكنون من الوصول إلى هذه الموارد أو نقاط النهاية.
استراتيجية Passport المحلية
قم بتشغيل الأوامر التالية:
npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local
npm install bcrypt --save
داخل مجلد auth، أنشئ ملفًا باسم local.strategy.ts وأضف التعليمات البرمجية التالية:
import { Strategy } from 'passport-local' ;
import { PassportStrategy } from '@nestjs/passport' ;
import { Injectable, UnauthorizedException } from '@nestjs/common' ;
import { AuthService } from './auth.service' ;
@Injectable ()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor ( private readonly authService: AuthService ) {
super ();
}
async validate(username: string , password: string ): Promise < any >{
const user = await this .authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException( 'Invalid user credentials' );
}
return user;
}
}
هنا، نقوم باستيراد Strategy و PassportStrategy و AuthService. نقوم بتوسيع PassportStrategy لإنشاء LocalStrategy. في حالتنا مع passport-local، لا توجد خيارات تهيئة، لذا يقوم المُنشئ (constructor) ببساطة باستدعاء super() بدون أي كائن خيارات. يجب علينا تطبيق دالة validate(). بالنسبة للاستراتيجية المحلية، يتوقع Passport دالة validate() بالتوقيع التالي: validate(username: string, password: string): any. يتم معظم عمل التحقق في خدمة المصادقة AuthService (بمساعدة UserService)، لذا فإن هذه الدالة مباشرة جدًا. نستدعي دالة validateUser() في AuthService (لم نكتب هذه الدالة بعد)، والتي تتحقق مما إذا كان المستخدم موجودًا وما إذا كانت كلمة المرور صحيحة. تُرجع authService.validateUser() قيمة null إذا كانت غير صالحة أو كائن المستخدم إذا كانت صالحة. إذا تم العثور على مستخدم وكانت بيانات الاعتماد صالحة، يتم إرجاع المستخدم حتى يتمكن Passport من إكمال مهامه (مثل إنشاء خاصية user على كائن Request)، ويمكن لخط أنابيب معالجة الطلب أن يستمر. إذا لم يتم العثور عليه، فإننا نُلقي استثناءً وندع طبقة الاستثناءات لدينا تتعامل معه.
الآن، أضف PassportModule و UsersModule و LocalStrategy إلى وحدة المصادقة (AuthModule) الخاصة بنا:
import { Module } from '@nestjs/common' ;
import { PassportModule } from '@nestjs/passport' ;
import { AuthService } from './auth.service' ;
import { AuthController } from './auth.controller' ;
import { UsersModule } from '../users/users.module' ;
import { LocalStrategy } from './local.strategy' ;
@Module ({
imports: [
PassportModule,
UsersModule,
],
providers: [
AuthService,
LocalStrategy,
],
controllers: [AuthController],
})
export class AuthModule {}
خدمة المصادقة (AuthService)
لنقم بتطبيق دالة validateUser():
import { Injectable } from '@nestjs/common' ;
import * as bcrypt from 'bcrypt' ;
import { UsersService } from '../users/users.service' ;
@Injectable ()
export class AuthService {
constructor ( private readonly userService: UsersService ) { }
async validateUser(username: string , pass: string ) {
// البحث عما إذا كان المستخدم موجودًا بهذا البريد الإلكتروني
const user = await this .userService.findOneByEmail(username);
if (!user) {
return null ;
}
// التحقق مما إذا كانت كلمة مرور المستخدم مطابقة
const match = await this .comparePassword(pass, user.password);
if (!match) {
return null ;
}
// tslint:disable-next-line: no-string-literal
const { password, ...result } = user[ 'dataValues' ];
return result;
}
private async comparePassword(enteredPassword, dbPassword) {
const match = await bcrypt.compare(enteredPassword, dbPassword);
return match;
}
}
هنا، نتحقق مما إذا كان المستخدم موجودًا بالبريد الإلكتروني المقدم. ثم نتحقق مما إذا كانت كلمة المرور في قاعدة البيانات (DB) تتطابق مع ما قدمه المستخدم. إذا فشل أي من هذه التحققات، فإننا نُرجع null، وإذا لم يكن كذلك، فإننا نُرجع كائن المستخدم.
comparePassword(enteredPassword, dbPassword):تقارن هذه الدالة الخاصة كلمة المرور التي أدخلها المستخدم بكلمة مرور المستخدم في قاعدة البيانات وتُرجع قيمة منطقية (boolean). إذا تطابقت كلمة المرور، تُرجعtrue، وإلا، تُرجعfalse.
استراتيجية Passport JWT
قم بتشغيل الأوامر التالية:
npm install @nestjs/jwt passport-jwt
npm install @types/passport-jwt --save-dev
داخل مجلد auth، أنشئ ملفًا باسم jwt.strategy.ts وأضف التعليمات البرمجية التالية:
import { ExtractJwt, Strategy } from 'passport-jwt' ;
import { PassportStrategy } from '@nestjs/passport' ;
import { Injectable, UnauthorizedException } from '@nestjs/common' ;
import { UsersService } from '../users/users.service' ;
@Injectable ()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor ( private readonly userService: UsersService ) {
super ({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false ,
secretOrKey: process.env.JWTKEY,
});
}
async validate(payload: any ) {
// التحقق مما إذا كان المستخدم في الرمز موجودًا بالفعل
const user = await this .userService.findOneById(payload.id);
if (!user) {
throw new UnauthorizedException(
'You are not authorized to perform the operation'
);
}
return payload;
}
}
هنا، نقوم بتوسيع PassportStrategy. داخل الدالة super()، أضفنا كائن خيارات. في حالتنا، هذه الخيارات هي:
jwtFromRequest:يوفر الطريقة التي سيتم بها استخراجJWTمن كائنRequest. سنستخدم النهج القياسي لتوفير رمز حامل (bearer token) في رأسAuthorizationلطلباتAPIالخاصة بنا.ignoreExpiration:لتوضيح الأمر، اخترنا الإعداد الافتراضيfalse، والذي يفوض مسؤولية التأكد من أنJWTلم تنته صلاحيته إلى وحدةPassport. هذا يعني أنه إذا تم تزويد مسارنا بـJWTمنتهي الصلاحية، فسيتم رفض الطلب وإرسال استجابة401 Unauthorized. يتعاملPassportمع هذا تلقائيًا وبشكل مريح لنا.secretOrKey:هذا هو مفتاحنا السري للرمز المميز. سيستخدم هذا المفتاح السري الموجود في ملف.envالخاص بنا.
بالنسبة لدالة validate(payload: any) في استراتيجية JWT، يتحقق Passport أولاً من توقيع JWT ويقوم بفك تشفير JSON. ثم يستدعي دالة validate() الخاصة بنا ويمرر JSON المفكك كمعامل وحيد لها. بناءً على طريقة عمل توقيع JWT، نضمن أننا نتلقى رمزًا صالحًا قمنا بتوقيعه وإصداره مسبقًا لمستخدم صالح. نؤكد ما إذا كان المستخدم موجودًا بمعرف حمولة المستخدم (user payload id). إذا كان المستخدم موجودًا، فإننا نُرجع كائن المستخدم، وسيقوم Passport بإرفاقه كخاصية على كائن Request. إذا لم يكن المستخدم موجودًا، فإننا نُلقي استثناءً.
الآن، أضف JwtStrategy و JwtModule إلى وحدة المصادقة (AuthModule) الخاصة بنا:
import { Module } from '@nestjs/common' ;
import { PassportModule } from '@nestjs/passport' ;
import { JwtModule } from '@nestjs/jwt' ;
import { AuthService } from './auth.service' ;
import { AuthController } from './auth.controller' ;
import { UsersModule } from '../users/users.module' ;
import { LocalStrategy } from './local.strategy' ;
import { JwtStrategy } from './jwt.strategy' ;
@Module ({
imports: [
PassportModule,
UsersModule,
JwtModule.register({
secret: process.env.JWTKEY,
signOptions: { expiresIn: process.env.TOKEN_EXPIRATION },
}),
],
providers: [
AuthService,
LocalStrategy,
JwtStrategy
],
controllers: [AuthController],
})
export class AuthModule { }
نقوم بتهيئة JwtModule باستخدام register()، مع تمرير كائن تهيئة. لنقم بإضافة دوال أخرى سنحتاجها لتسجيل الدخول وإنشاء مستخدم جديد في AuthService:
import { Injectable } from '@nestjs/common' ;
import * as bcrypt from 'bcrypt' ;
import { JwtService } from '@nestjs/jwt' ;
import { UsersService } from '../users/users.service' ;
@Injectable ()
export class AuthService {
constructor (
private readonly userService: UsersService,
private readonly jwtService: JwtService,
) { }
async validateUser(username: string , pass: string ) {
// البحث عما إذا كان المستخدم موجودًا بهذا البريد الإلكتروني
const user = await this .userService.findOneByEmail(username);
if (!user) {
return null ;
}
// التحقق مما إذا كانت كلمة مرور المستخدم مطابقة
const match = await this .comparePassword(pass, user.password);
if (!match) {
return null ;
}
// tslint:disable-next-line: no-string-literal
const { password, ...result } = user[ 'dataValues' ];
return result;
}
public async login(user) {
const token = await this .generateToken(user);
return { user, token };
}
public async create(user) {
// تشفير كلمة المرور
const pass = await this .hashPassword(user.password);
// إنشاء المستخدم
const newUser = await this .userService.create({ ...user, password: pass });
// tslint:disable-next-line: no-string-literal
const { password, ...result } = newUser[ 'dataValues' ];
// إنشاء الرمز المميز
const token = await this .generateToken(result);
// إرجاع المستخدم والرمز المميز
return { user: result, token };
}
private async generateToken(user) {
const token = await this .jwtService.signAsync(user);
return token;
}
private async hashPassword(password) {
const hash = await bcrypt.hash(password, 10 );
return hash;
}
private async comparePassword(enteredPassword, dbPassword) {
const match = await bcrypt.compare(enteredPassword, dbPassword);
return match;
}
}
نقوم باستيراد وحقن JwtService.
login(user):تُستخدم هذه الدالة لتسجيل دخول المستخدم. تأخذ معلومات المستخدم، وتُنشئ رمزًا مميزًا (token) به، ثم تُرجع الرمز المميز وكائن المستخدم.create(user):تُستخدم هذه الدالة لإنشاء مستخدم جديد. تأخذ معلومات المستخدم، وتُشفر كلمة مرور المستخدم، وتحفظ المستخدم في قاعدة البيانات، وتُزيل كلمة المرور من كائن المستخدم المُرجع حديثًا، وتُنشئ رمزًا مميزًا بكائن المستخدم، ثم تُرجع الرمز المميز وكائن المستخدم.generateToken(user):تُنشئ هذه الدالة الخاصة رمزًا مميزًا ثم تُرجعه.hashPassword(password):تُشفر هذه الدالة الخاصة كلمة مرور المستخدم وتُرجع كلمة المرور المشفرة.
سنستخدم جميع هذه الدوال لاحقًا.
متحكم المصادقة (AuthController)
الآن، لنقم بإنشاء دالتي signup و login الخاصتين بنا:
import { Controller, Body, Post, UseGuards, Request } from '@nestjs/common' ;
import { AuthGuard } from '@nestjs/passport' ;
import { AuthService } from './auth.service' ;
import { UserDto } from '../users/dto/user.dto' ;
@Controller ( 'auth' )
export class AuthController {
constructor ( private authService: AuthService ) {}
@UseGuards (AuthGuard( 'local' ))
@Post ( 'login' )
async login( @Request () req) {
return await this .authService.login(req.user);
}
@Post ( 'signup' )
async signUp( @Body () user: UserDto) {
return await this .authService.create(user);
}
}
عند استدعاء نقطة النهاية POST api/v1/auth/login، سيتم استدعاء المُزين @UseGuards(AuthGuard('local')). سيأخذ هذا البريد الإلكتروني/اسم المستخدم وكلمة المرور، ثم يقوم بتشغيل دالة validate في فئة الاستراتيجية المحلية (local strategy class) الخاصة بنا. ستقوم دالة login(@Request() req) بإنشاء رمز JWT وإرجاعه. نقطة النهاية POST api/v1/auth/signup ستستدعي دالة this.authService.create(user)، وتُنشئ المستخدم، وتُرجع رمز JWT.
تجربة المصادقة
لنقم بتجربتها… افتح تطبيق Postman الخاص بك وتأكد من أنه يعمل. أرسل طلب POST إلى http://localhost:3000/api/v1/auth/signup وأدخل بيانات الجسم (body data) لإنشاء مستخدم. يجب أن تحصل على رمز مميز (token) وكائن المستخدم (user object) كاستجابة.

الآن بعد أن أصبح لدينا مستخدم، لنقم بتسجيل دخوله. أرسل طلب POST إلى http://localhost:3000/api/v1/auth/login وأدخل اسم المستخدم وكلمة المرور فقط. يجب أن تحصل على رمز مميز (token) وكائن المستخدم (user object) كاستجابة.

التحقق من صحة البيانات (Validation)
لاحظ كيف أننا لا نتحقق من صحة أي من مدخلات المستخدم. الآن، لنقم بإضافة التحقق من صحة البيانات إلى تطبيقنا. قم بتشغيل الأمر التالي:
npm i class-validator class-transformer --save
داخل مجلد core، أنشئ مجلدًا باسم pipes ثم أنشئ ملفًا باسم validate.pipe.ts. انسخ والصق التعليمات البرمجية التالية:
import { Injectable, ArgumentMetadata, BadRequestException, ValidationPipe, UnprocessableEntityException } from '@nestjs/common' ;
@Injectable ()
export class ValidateInputPipe extends ValidationPipe {
public async transform(value, metadata: ArgumentMetadata) {
try {
return await super .transform(value, metadata);
} catch (e) {
if (e instanceof BadRequestException) {
throw new UnprocessableEntityException(
this .handleError(e.message.message)
);
}
}
}
private handleError(errors) {
return errors.map(
error => error.constraints
);
}
}
لنقم بالتحقق التلقائي من صحة جميع نقاط النهاية باستخدام كائنات نقل البيانات (DTO) عن طريق ربط ValidateInputPipe على مستوى التطبيق. داخل ملف main.ts، أضف هذا:
import { NestFactory } from '@nestjs/core' ;
import { AppModule } from './app.module' ;
import { ValidateInputPipe } from './core/pipes/validate.pipe' ;
async function bootstrap ( ) {
const app = await NestFactory.create(AppModule);
// global endpoints prefix
app.setGlobalPrefix( 'api/v1' );
// handle all user input validation globally
app.useGlobalPipes( new ValidateInputPipe());
await app.listen( 3000 );
}
bootstrap();
الآن، لنقم بتحديث ملف DTO الخاص بالمستخدمين:
import { IsNotEmpty, MinLength, IsEmail, IsEnum } from 'class-validator' ;
enum Gender {
MALE = 'male' ,
FEMALE = 'female' ,
}
export class UserDto {
@IsNotEmpty ()
readonly name: string ;
@IsNotEmpty ()
@IsEmail ()
readonly email: string ;
@IsNotEmpty ()
@MinLength ( 6 )
readonly password: string ;
@IsNotEmpty ()
@IsEnum (Gender, { message: 'gender must be either male or female' , })
readonly gender: Gender;
}
هنا، نقوم باستيراد هذه المُزينات (decorators) من مكتبة class-validator.
@IsNotEmpty():يضمن أن الحقل ليس فارغًا.@IsEmail():يتحقق مما إذا كان البريد الإلكتروني المدخل عنوان بريد إلكتروني صالحًا.@MinLength(6):يضمن أن طول كلمة المرور لا يقل عن ستة أحرف.@IsEnum:يضمن السماح بالقيم المحددة فقط (في هذه الحالة،maleوfemale).
تحتوي مكتبة class-validator على العديد من مُزينات التحقق من صحة البيانات – يمكنك استكشافها. لنقم بتجربة التحقق من صحة البيانات لدينا…

بدون تمرير أي قيمة، تلقيت خطأ التحقق من الصحة التالي. التحقق من صحة البيانات لدينا يعمل الآن. هذا التحقق تلقائي لجميع نقاط النهاية التي تستخدم كائن نقل البيانات (DTO).
حساب مستخدم فريد (Unique User Account)
لنقم بإضافة حارس (guard) يمنع المستخدمين من التسجيل بنفس البريد الإلكتروني مرتين، نظرًا لأن البريد الإلكتروني فريد على مستوى المخطط (schema level). داخل مجلد core، أنشئ مجلدًا باسم guards، ثم أنشئ ملفًا باسم doesUserExist.guard.ts. انسخ والصق التعليمات البرمجية التالية:
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common' ;
import { Observable } from 'rxjs' ;
import { UsersService } from '../../modules/users/users.service' ;
@Injectable ()
export class DoesUserExist implements CanActivate {
constructor ( private readonly userService: UsersService ) {}
canActivate(
context: ExecutionContext,
): boolean | Promise < boolean > | Observable< boolean > {
const request = context.switchToHttp().getRequest();
return this .validateRequest(request);
}
async validateRequest(request) {
const userExist = await this .userService.findOneByEmail(request.body.email);
if (userExist) {
throw new ForbiddenException( 'This email already exist' );
}
return true ;
}
}
الآن، لنقم بإضافة هذا الحارس إلى دالة التسجيل (signup method) في AuthController:
import { Controller, Body, Post, UseGuards, Request } from '@nestjs/common' ;
import { AuthGuard } from '@nestjs/passport' ;
import { AuthService } from './auth.service' ;
import { UserDto } from '../users/dto/user.dto' ;
import { DoesUserExist } from '../../core/guards/doesUserExist.guard' ;
@Controller ( 'auth' )
export class AuthController {
constructor ( private authService: AuthService ) { }
@UseGuards (AuthGuard( 'local' ))
@Post ( 'login' )
async login( @Request () req) {
return await this .authService.login(req.user);
}
@UseGuards (DoesUserExist)
@Post ( 'signup' )
async signUp( @Body () user: UserDto) {
return await this .authService.create(user);
}
}
لنحاول الآن إنشاء مستخدم ببريد إلكتروني موجود بالفعل في قاعدة بياناتنا:

وحدة المنشورات (Post Module)
إنشاء وحدة المنشورات
قم بتشغيل الأمر التالي لإنشاء وحدة المنشورات:
nest g module /modules/posts
سيؤدي هذا إلى إضافة هذه الوحدة تلقائيًا إلى وحدتنا الجذرية AppModule.
إنشاء خدمة المنشورات (Post Service)
قم بتشغيل الأمر التالي لإنشاء خدمة المنشورات:
nest g service /modules/posts
سيؤدي هذا إلى إضافة هذه الخدمة تلقائيًا إلى وحدة المنشورات (Post module).
إنشاء متحكم المنشورات (Post Controller)
قم بتشغيل الأمر التالي لإنشاء متحكم المنشورات:
nest g co /modules/posts
سيؤدي هذا إلى إضافة هذا المتحكم تلقائيًا إلى وحدة المنشورات.
كيان المنشور (Post Entity)
أنشئ ملف post.entity.ts داخل مجلد posts. انسخ والصق التعليمات البرمجية التالية:
import { Table, Column, Model, DataType, ForeignKey, BelongsTo } from 'sequelize-typescript' ;
import { User } from '../users/user.entity' ;
@Table
export class Post extends Model<Post> {
@Column ({
type : DataType.STRING,
allowNull: false ,
})
title: string ;
@Column ({
type : DataType.TEXT,
allowNull: false ,
})
body: string ;
@ForeignKey ( () => User)
@Column ({
type : DataType.INTEGER,
allowNull: false ,
})
userId: number ;
@BelongsTo ( () => User)
user: User;
}
الشيء الجديد الوحيد هنا هو المُزين @ForeignKey(() => User) الذي يحدد أن عمود userId هو معرف جدول المستخدم (User table)، والمُزين @BelongsTo(() => User) الذي يحدد العلاقة بين جدول المنشور (Post table) وجدول المستخدم.
كائن نقل بيانات المنشور (Post DTO – Data Transfer Object)
داخل مجلد posts، أنشئ مجلدًا باسم dto ثم أنشئ ملفًا باسم post.dto.ts بداخله. انسخ والصق التعليمات البرمجية التالية:
import { IsNotEmpty, MinLength } from 'class-validator' ;
export class PostDto {
@IsNotEmpty ()
@MinLength ( 4 )
readonly title: string ;
@IsNotEmpty ()
readonly body: string ;
}
هنا، يجب أن يحتوي كائن جسم المنشور (post body object) الخاص بنا على عنوان (title) وجسم (body)، ويجب ألا يقل طول العنوان عن 4 أحرف.
مزود المنشورات (Post Provider)
أنشئ ملف posts.providers.ts داخل مجلد posts. انسخ والصق التعليمات البرمجية التالية:
import { Post } from './post.entity' ;
import { POST_REPOSITORY } from '../../core/constants' ;
export const postsProviders = [{
provide: POST_REPOSITORY,
useValue: Post,
}];
أضف السطر export const POST_REPOSITORY = 'POST_REPOSITORY'; إلى ملف الثوابت index.ts. أضف مزود المنشورات الخاص بنا إلى ملف وحدة المنشورات (Post Module):
import { Module } from '@nestjs/common' ;
import { PostsService } from './posts.service' ;
import { PostsController } from './posts.controller' ;
import { postsProviders } from './posts.providers' ;
@Module ({
providers: [PostsService, ...postsProviders],
controllers: [PostsController],
})
export class PostsModule { }
الآن، أضف كيان المنشور (Post entity) الخاص بنا إلى مزود قاعدة البيانات. استورد كيان المنشور داخل ملف database.providers.ts، وأضف Post إلى هذه الدالة: sequelize.addModels([User, Post]);.
دوال خدمة المنشورات (Post Service Methods)
انسخ والصق التعليمات البرمجية التالية داخل ملف خدمة المنشورات (Post service file):
import { Injectable, Inject } from '@nestjs/common' ;
import { Post } from './post.entity' ;
import { PostDto } from './dto/post.dto' ;
import { User } from '../users/user.entity' ;
import { POST_REPOSITORY } from '../../core/constants' ;
@Injectable ()
export class PostsService {
constructor ( @Inject (POST_REPOSITORY) private readonly postRepository: typeof Post ) { }
async create(post: PostDto, userId): Promise <Post> {
return await this .postRepository.create<Post>({ ...post, userId });
}
async findAll(): Promise <Post[]> {
return await this .postRepository.findAll<Post>({
include: [{ model: User, attributes: { exclude: [ 'password' ] } }],
});
}
async findOne(id): Promise <Post> {
return await this .postRepository.findOne({
where: { id },
include: [{ model: User, attributes: { exclude: [ 'password' ] } }],
});
}
async delete (id, userId) {
return await this .postRepository.destroy({ where: { id, userId } });
}
async update(id, data, userId) {
const [numberOfAffectedRows, [updatedPost]] = await this .postRepository.update({
...data
}, {
where: { id, userId },
returning: true
});
return { numberOfAffectedRows, updatedPost };
}
}
هنا، نقوم بحقن مستودع المنشورات (Post repository) الخاص بنا للتواصل مع قاعدة البيانات.
create(post: PostDto, userId):تقبل هذه الدالة كائن المنشور (post object) ومعرف المستخدم (userId) الذي يقوم بإنشاء المنشور. تُضيف المنشور إلى قاعدة البيانات وتُرجع المنشور الذي تم إنشاؤه حديثًا.PostDtoمخصص للتحقق من صحة البيانات.findAll():تجلب هذه الدالة جميع المنشورات من قاعدة البيانات وتُضمن (eager load) المستخدم الذي أنشأها مع استبعاد كلمة مرور المستخدم.findOne(id):تبحث هذه الدالة عن المنشور بالمعرف (id) وتُرجعه. كما تُضمن المستخدم الذي أنشأه مع استبعاد كلمة مرور المستخدم.delete(id, userId):تحذف هذه الدالة المنشور من قاعدة البيانات باستخدام المعرف (id) ومعرف المستخدم (userId). فقط المستخدم الذي أنشأ المنشور يمكنه حذفه. تُرجع هذه الدالة عدد الصفوف التي تأثرت.update(id, data, userId):تُحدث هذه الدالة منشورًا موجودًا حيثidهو معرف المنشور، وdataهي البيانات المراد تحديثها، وuserIdهو معرف المُنشئ الأصلي. تُرجع هذه الدالة عدد الصفوف التي تم تحديثها والكائن المحدث حديثًا.
دوال متحكم المنشورات (Post Controller Methods)
انسخ والصق التعليمات البرمجية التالية داخل ملف متحكم المنشورات (Post controller file):
import { Controller, Get, Post, Put, Delete, Param, Body, NotFoundException, UseGuards, Request } from '@nestjs/common' ;
import { AuthGuard } from '@nestjs/passport' ;
import { PostsService } from './posts.service' ;
import { Post as PostEntity } from './post.entity' ;
import { PostDto } from './dto/post.dto' ;
@Controller ( 'posts' )
export class PostsController {
constructor ( private readonly postService: PostsService ) { }
@Get ()
async findAll() {
// جلب جميع المنشورات من قاعدة البيانات
return await this .postService.findAll();
}
@Get ( ':id' )
async findOne( @Param ( 'id' ) id: number ): Promise <PostEntity> {
// البحث عن المنشور بهذا المعرف
const post = await this .postService.findOne(id);
// إذا لم يكن المنشور موجودًا في قاعدة البيانات، ألقِ خطأ 404
if (!post) {
throw new NotFoundException( 'This Post doesn\'t exist' );
}
// إذا كان المنشور موجودًا، أرجعه
return post;
}
@UseGuards (AuthGuard( 'jwt' ))
@Post ()
async create( @Body () post: PostDto, @Request () req): Promise <PostEntity> {
// إنشاء منشور جديد وإرجاع المنشور الذي تم إنشاؤه حديثًا
return await this .postService.create(post, req.user.id);
}
@UseGuards (AuthGuard( 'jwt' ))
@Put ( ':id' )
async update( @Param ( 'id' ) id: number , @Body () post: PostDto, @Request () req): Promise <PostEntity> {
// الحصول على عدد الصفوف المتأثرة والمنشور المحدث
const { numberOfAffectedRows, updatedPost } = await this .postService.update(id, post, req.user.id);
// إذا كان عدد الصفوف المتأثرة صفرًا،
// فهذا يعني أن المنشور غير موجود في قاعدة بياناتنا
if (numberOfAffectedRows === 0 ) {
throw new NotFoundException( 'This Post doesn\'t exist' );
}
// إرجاع المنشور المحدث
return updatedPost;
}
@UseGuards (AuthGuard( 'jwt' ))
@Delete ( ':id' )
async remove( @Param ( 'id' ) id: number , @Request () req) {
// حذف المنشور بهذا المعرف
const deleted = await this .postService.delete(id, req.user.id);
// إذا كان عدد الصفوف المتأثرة صفرًا،
// فهذا يعني أن المنشور غير موجود في قاعدة بياناتنا
if (deleted === 0 ) {
throw new NotFoundException( 'This Post doesn\'t exist' );
}
// إرجاع رسالة نجاح
return 'Successfully deleted' ;
}
}
يتم تنفيذ معظم وظائف عمليات CRUD في خدمة المنشورات (PostService) الخاصة بنا.
findAll():تتعامل هذه الدالة مع طلبGETإلى نقطة النهايةapi/v1/posts. تُرجع جميع المنشورات في قاعدة بياناتنا.findOne(@Param('id') id: number):تتعامل هذه الدالة مع طلبGETإلى نقطة النهايةapi/v1/posts/1لجلب منشور واحد، حيث1هو معرف المنشور. تُلقي هذه الدالة خطأ404إذا لم تعثر على المنشور وتُرجع كائن المنشور إذا عثرت عليه.create(@Body() post: PostDto, @Request() req):تتعامل هذه الدالة مع طلبPOSTإلى نقطة النهايةapi/v1/postsلإنشاء منشور جديد. يُستخدم المُزين@UseGuards(AuthGuard('jwt'))لحماية المسار (تذكر استراتيجيةJWTالخاصة بنا). فقط المستخدمون المسجلون يمكنهم إنشاء منشور.update(@Param('id') id: number, @Body() post: PostDto, @Request() req):تتعامل هذه الدالة مع طلبPUTإلى نقطة النهايةapi/v1/postsلتحديث منشور موجود. إنه أيضًا مسار محمي. إذا كانnumberOfAffectedRowsصفرًا، فهذا يعني أنه لم يتم العثور على منشور بالمعرفات المحددة.remove(@Param('id') id: number, @Request() req):تتعامل هذه الدالة مع طلبDELETEلحذف منشور موجود.
تجربة عمليات CRUD
لنقم بتجربة عمليات CRUD الخاصة بنا…
إنشاء منشور (Create a Post)
سجل الدخول وأضف الرمز المميز (token) الخاص بك لأن مسار إنشاء المنشور هو مسار محمي.


إنشاء منشور.
قراءة منشور واحد (Read a single Post)
هذا المسار غير محمي، لذا يمكن الوصول إليه بدون الرمز المميز.

جلب منشور واحد.
قراءة جميع المنشورات (Reading all Posts)
هذا المسار غير محمي، لذا يمكن الوصول إليه بدون الرمز المميز أيضًا.

جلب جميع المنشورات.
تحديث منشور واحد (Updating a Single Post)
هذا المسار محمي، لذا نحتاج إلى رمز مميز، وفقط المُنشئ يمكنه تحديثه.

تحديث منشور واحد.
حذف منشور (Deleting a Post)
هذا المسار محمي، لذا نحتاج إلى رمز مميز، وفقط المُنشئ يمكنه حذفه.

حذف منشور.
الخلاصة التقنية
يقدم NestJS طريقة أكثر تنظيمًا وفعالية لبناء تطبيقات الخادم (server-side applications) باستخدام Node.js. من خلال تبني مبادئ التصميم المعيارية (modular design) والبرمجة الشيئية (object-oriented programming)، يسهل NestJS تطوير واجهات برمجة التطبيقات القابلة للتوسع والصيانة. إن دمجه مع PostgreSQL كقاعدة بيانات قوية و Sequelize كـ ORM فعال يوفر حزمة متكاملة للمطورين لإنشاء تطبيقات ويب قوية وموثوقة. هذا الدليل يمثل نقطة انطلاق ممتازة لأي مبتدئ يرغب في الغوص في عالم تطوير APIs الحديثة باستخدام هذه التقنيات.
لمزيد من المعلومات، يمكنك زيارة الموقع الرسمي لـ NestJS. نأمل أن يكون هذا المقال قد قدم لك قيمة مضافة وفائدة حقيقية في رحلتك التعليمية. يمكنك التواصل مع المؤلف عبر LinkedIn و Twitter.