اذهب إلى المحتوى

بناء واجهة برمجية متوافقة مع REST باستخدام Express.js -الجزء الأول


ابراهيم الخضور

نتحدث في هذه السلسلة من المقالات عن خطوات بناء تطبيق بسيط يمثل واجهة خلفية على هيئة واجهة برمجية API باستخدام إطار عمل Express.js ولغة البرمجة  TypeScript.

كيف أكتب واجهة برمجية REST في بيئة Node.js

غالبًا ما تكون المكتبة Express.js هي الخيار اﻷول من بين إطارات عمل Node.js عند كتابة واجهة خلفية لتكون واجهة برمجية REST. وعلى الرغم من أنها تدعم أيضًا بناء صفحات وقوالب HTML، لكننا سنركز في هذه السلسلة من المقالات على بناء واجهة خلفية باستخدام لغة TypeScript كي نسمح لأي واجهة أمامية أو واجهة خلفية خارجية (خادم آخر) من الاستعلام منها، لهذا عليك أن:

سنبدأ من الطرفية أو (محرر سطر اﻷوامر) لإنشاء مجلّد خاص بالمشروع، ثم ننفّذ اﻷمر

 run npm init 

يُنشئ هذا الأمر بعض الملفات الأساسية التي نحتاجها لمشروع Node.js.

ثم نضيف بعد ذلك إطار العمل Express.js وبعض المكتبات المفيدة اﻷخرى لمشروعنا من خلال الأمر التالي:

npm i express debug winston express-winston cors

وبالطبع هناك أسباب وجيهة كي يفضّل مطوّرو Node.js المكتبات السابقة:

  • debug: هي وحدة برمجية تُستخدم لتفادي استخدام اﻷمر ()console.log أثناء تطوير التطبيقات. إذ تستخدم لترشيح العبارات التي نريد تنقيحها عند محاولة حل المشاكل التي تواجهنا. وباﻹمكان إيقافها كليًا في نسخة اﻹنتاج بدلًا من إزالتها يدويًا.
  • winston: الوحدة المسؤولة عن تسجيل الطلبات القادمة إلى الواجهة البرمجية والاستجابات (واﻷخطاء) التي تعيدها الواجهة. وتتكامل
    express-winston مباشرة مع Express.js لهذا ستكون شيفرة واجهة برمجية المتعلقة بعملية إدارة السجلات التي تؤديها winston جاهزة.
  • cors: هي جزء من أداة Express.js الوسيطة والتي تسمح لنا بمشاركة الموارد ذات اﻷصول المختلطة cross-origin resource sharing. وبدون هذه المكتبة لن تتمكن الواجهة البرمجية من تخديم سوى الواجهة الأمامية الموجودة في نفس النطاق الفرعي الذي يحوي الواجهة الخلفية.

تستخدم الواجهة الخلفية في تطبيقنا تلك الحزم عند يعمل التطبيق، لهذا علينا تثبيت بعض اعتماديات مرحلة التطوير لتهيئة TypeScript. لهذا ننفذ الأمر التالي:

npm i --save-dev @types/cors @types/express @types/debug source-map-support tslint typescript

نحتاج الاعتماديات السابقة لتفعيل TypeScript في شيفرة تطبيقنا، واﻷنواع التي تستخدمها Express.js والاعتماديات اﻷخرى. وسيوّفر هذا اﻷمر وقتك عند استخدام بيئات تطوير متكاملة مثل WebStorm أو VSCode التي تتيح لك ميزات اﻹكمال التلقائي لبعض التوابع أثناء كتابة الشيفرة.

ينبغي أن تكون الاعتمادات بشكلها النهائي ضمن الملف package.json كالتالي:

"dependencies": {
    "debug": "^4.2.0",
    "express": "^4.17.1",
    "express-winston": "^4.0.5",
    "winston": "^3.3.3",
    "cors": "^2.8.5"
},
"devDependencies": {
    "@types/cors": "^2.8.7",
    "@types/debug": "^4.1.5",
    "@types/express": "^4.17.2",
    "source-map-support": "^0.5.16",
    "tslint": "^6.0.0",
    "typescript": "^3.7.5"
}

وهكذا تكون جميع الاعتماديات اللازمة لعمل تطبيقنا جاهزة!

هيكيلية مشروع واجهة برمجية REST باستخدام TypeScript

سنستخدم في مشروعنا ثلاث ملفات وهي:

  1. app.ts/.
  2. common/common.routes.config.ts/.
  3. users/users.routes.config.ts/.

إن الفكرة من استخدام مجلدين (common و users) في مشروعنا هو تكوين وحدتين لكل منهما مسؤولياتها الخاصة. وبالتالي قد نعطي الوحدتين بعض أو كل الميزات التالية:

  • تهيئة الوجهة Rourte: لتعريف الطلبات التي يمكن أن تتعامل معها الواجهة البرمجية.
  • خدمات Services: لتنفيذ مهام مثل الاتصال بقاعدة البيانات وتنفيذ استعلامات أو الاتصال بخدمات خارجية ضرورية للاستعلام.
  • وسيط أو برمجية وسيطة middleware: للتحقق من صلاحية طلب معيّن قبل أن يتعامل المتحكم النهائي بالمسار مع تفاصيل الاستعلام.
  • وحدات Models: لتعريف وحدات البيانات التي تطابق تخطيط قاعدة بيانات محددة، لتسهيل تخزين البيانات واستعادتها.
  • متحكّمات controllers: لفصل معلومات تهيئة المسار أو الوجهة التي سننتقل إليها route configuration عن الشيفرة التي تعالج في النهاية (بعد المرور على أية برامج وسيطة) طلب هذا المسار أو تستدعي دوال خدمة من مستوى أعلى عند الحاجة، وتعيد الاستجابة على هذا الطلب إلى العميل.

للمجلد هيكلية ذات تصميم بسيط متوافق مع الواجهة البرمجية.

ملفات مسارات Routes شائعة الاستخدام في TypeScript

سنعمل على تنظيم ملفات Routes في تطبيق Express.js ،لذا سننشئ الملف common.routes.config.ts في المجلّد common ونضع فيه الشيفرة التالية:

import express from 'express';
export class CommonRoutesConfig {
    app: express.Application;
    name: string;

    constructor(app: express.Application, name: string) {
        this.app = app;
        this.name = name;
    }
    getName() {
        return this.name;
    }
}

إن الطريقة التي ننشئ فيها المسارات routes هنا اختيارية، لكن، وطالما أننا نعمل مع TypeScript، فمن الجيد أن نتدرب على بناء المسارات باستخدام الوراثة من خلال التعليمة extends كما سنرى بعد قليل.

إذ ننشئ صنفًا رئيسيًا لتحديد سلوك وملامح مشتركة بين جميع مسارات التطبيق وسيكون لجميع الملفات في هذا المشروع السلوك ذاته، كما سيكون لها اسم وإمكانية الوصول إلى كائن Express.js اﻷساسي في تطبيقنا Application ثم ننشئ صنفًا فرعيًا منه لتحديد سلوك مسارات معينة.

الآن، يمكننا أن نبدأ في إنشاء ملف مسارات المستخدمين  في مجلد المستخدمين users، دعونا ننشئ ملف باسم users.routes.config.ts ونكتب بداخله الشيفرة البرمجية التالية

import {CommonRoutesConfig} from '../common/common.routes.config';
import express from 'express';

export class UsersRoutes extends CommonRoutesConfig {
    constructor(app: express.Application) {
        super(app, 'UsersRoutes');
    }
}

هنا، نقوم باستيراد الصنف CommonRoutesConfig  ونوسعه إلى صنف جديد UsersRoutes. ونرسل من خلال الدالة البنائية constructor التطبيق (أي كائن express.Application الرئيسي) واسم UsersRoutes إلى دالة البناء الخاصة بـ CommonRoutesConfig.

قد يبدو المثال بسيطًا لكن عند توسيع الأمر ليشمل عدة ملفات routes سيساعدنا ذلك في تفادي تكرار الشيفرة.

لنفترض أننا سنحتاج إلى إضافة ميزات جديدة في هذا الملف، مثل إدارة السجلات وتسجيل الأحداث، عندها بإمكاننا إضافة الحقول الضرورية إلى الصنف CommonRoutesConfig وعندها ستتمكن جميع اﻷصناف المشتقة منه من الوصول إلى هذه الميزة.

استخدام دوال TypeScript المجرّدة لتقديم وظائف متشابه بين اﻷصناف

ماذا لو أردنا الحصول على وظائف متشابهة ضمن اﻷصناف المختلفة (مثل تهيئة نقاط الاتصال بالواجهات البرمجية)، على الرغم من اختلاف طرق تنفيذ هذه الوظائف من صنف ﻵخر؟ أحد الخيارات المتاحة هو استخدام ميزة تُدعى التجريد abstraction في TypeScript.

لنحاول إنشاء دالة مجرّدة بسيطة جدًا يرثها الصنف UsersRoutes (وبقية أصناف التوجيه التي قد ننشئها لاحقًا) من الصنف CommonRoutesConfig. ولنفترض أننا سنجبر كل الوجهات على امتلاك دالة (حتى نتمكن من استدعائها من الدالة البانية المشتركة) تُدعى()configureRoutes، وفيها نصرّح عن نقاط الوصول الخاصة بكل مورد من موارد الصنف. ولتنفيذ اﻷمر، سنضيف هذه اﻷشياء إلى الملف common.routes.config.ts:

  1. الكلمة المحجوزة abstract إلى السطر الذي يضم الكلمة class كي نفعّل خاصية التجريد لهذا الصنف.
  2. تصريح عن دالة جديدة في نهاية الصنف abstract configureRoutes(): express.Application، تجبر أي صنف مشتق من الصنف CommonRoutesConfig على تقديم آلية تطابق توقيع الدالة function signature، وسيرمي مصرّف TypeScript خطأ إن لم يجد آلية كهذه.
  3. استدعاء الدالة ()this.configureRoutes في نهاية الدالة البانية طالما أننا متأكدين من وجود هذه الدالة.

ستبدو الشيفرة اﻵن كالتالي:

import express from 'express';
export abstract class CommonRoutesConfig {
    app: express.Application;
    name: string;

    constructor(app: express.Application, name: string) {
        this.app = app;
        this.name = name;
        this.configureRoutes();
    }
    getName() {
        return this.name;
    }
    abstract configureRoutes(): express.Application;
}

وهكذا ينبغي على كل صنف مشتق من الصنف CommonRoutesConfig أن يمتلك دالة ()configureRoutes تُدعى تُعيد كائنًا من النوع express.Application، وبالتالي لا بد من تحديث الملف users.routes.config.ts:

import {CommonRoutesConfig} from '../common/common.routes.config';
import express from 'express';

export class UsersRoutes extends CommonRoutesConfig {
    constructor(app: express.Application) {
        super(app, 'UsersRoutes');
    }

    configureRoutes() {
        // (we'll add the actual route configuration here next)
        return this.app;
    }

}

دعنا نراجع ما فعلناه حتى الآن:

أدرجنا بداية الملف common.routes.config ومن ثم الوحدة البرمجية express، وعرفنا بعد ذلك الصنف UserRoutes الذي أردناه أن يرث الصنف اﻷساسي CommonRoutesConfig وبالتالي سيضمّ الدالة ()configureRoutes ويقدّم آلية لتنفيذها.

وﻹرسال المعلومات عبر الصنف CommonRoutesConfig، نستخدم الدالة البانية للصنف التي تتوقع تمرير كائن express.Application إليها، وهذا ما سنشرحه بتفاصيل أكثر لاحقًا. نمرر من خلال الدالة ()super التطبيق إلى الدالة البانية للصنف CommonRoutesConfig واسم الوجهة (وهي في هذه الحالة UsersRoutes). وتستدعي الدالة ()super بدورها الدالة ()configureRoutes.

تهيئة وجهات Express.js الخاصة بنقاط الوصول إلى المستخدمين

ستكون الدالة المكان الذي ننشئ فيه نقاط الوصول بين المستخدم والواجهة البرمجية REST. وفيها نستخدم التطبيق مع وظائف التوجيه من خلال Express.js.

تكمن الفكرة في استخدام الدالة ()app.route لتفادي تكرار الشيفرة، وهذا اﻷمر سهل نسبيًا طالما أننا ننشئ واجهة برمجية REST ذات موارد محددة تمامًا. إن المورد اﻷساسي في تطبيقنا هو users، ولدينا حالتان:

  • عندما يريد مستدعي الواجهة البرمجية إنشاء مستخدم جديد أو الحصول على قائمة بالمستخدمين الموجودين، لا بد أن يكون اسم المورد فقط users في نهاية المسار إلى المورد (لا نريد الخوض في هذه الحالة في فلترة أو تنظيم نتائج الاستعلام أو غيرها من العمليات في هذا التطبيق).
  • عندما يريد المستدعي أن ينفّذ عملية ما على سجل مستخدم، وعندها لابد أن يكون نمط المسار إلى المورد كالتالي: users/:userId.

تتيح آلية عمل الدالة ()route. في Express.js التعامل مع طلبات HTTP بأسلوب متسلسل أنيق، لأن جميع التوابع ()get. و ()post. وغيرها، ستعيد نفس النسخة من الكائن التي يعيدها التابع ()route.. لهذا سننهي عملية التهيئة كالتالي:

configureRoutes() {

    this.app.route(`/users`)
        .get((req: express.Request, res: express.Response) => {
            res.status(200).send(`List of users`);
        })
        .post((req: express.Request, res: express.Response) => {
            res.status(200).send(`Post to users`);
        });

    this.app.route(`/users/:userId`)
        .all((req: express.Request, res: express.Response, next: express.NextFunction) => {
            // /users/:userId يُنفّذ البرنامج الوسيط هذه الدالة قبل أي استعلام
            // لكنه لا ينفذ شيئًا اﻵن
            // next() بل يمرر ببساطة التحكم إلى الدالة التالية في التطبيق تحت    

        next();
        })
        .get((req: express.Request, res: express.Response) => {
            res.status(200).send(`GET requested for id ${req.params.userId}`);
        })
        .put((req: express.Request, res: express.Response) => {
            res.status(200).send(`PUT requested for id ${req.params.userId}`);
        })
        .patch((req: express.Request, res: express.Response) => {
            res.status(200).send(`PATCH requested for id ${req.params.userId}`);
        })
        .delete((req: express.Request, res: express.Response) => {
            res.status(200).send(`DELETE requested for id ${req.params.userId}`);
        });

    return this.app;
}

تتيح الشيفرة السابقة لعميل الواجهة البرمجية المتوافقة مع REST استدعاء نقطة الوصول users باستخدام أحد الاستعلامين POST أو GET، وتتيح له بنفس اﻷسلوب استدعاء نقطة الوصول users/:userId من خلال استعلامات GET أو PUT أو PATCH أو DELETE.

كما أضفنا إلى نقطة الوصول users/:userId برنامج وسيط يستخدم الدالة ()all التي تُنفَّذ قبل أي استدعاء للدوال ()get أو ()put أو ()patch أو ()delete. وسيكون لهذه الدالة أهميتها عندما ننشئ لاحقًا مسارات يصل إليها فقط المستخدمين المستوثقين.

وقد تلاحظ أن جميع الدوال ()all -وأية أجزاء من البرنامج الوسيط- تمتلك ثلاثة أنواع من الحقول Request و Response و NextFunction:

  • النوع Request هو طريقة Express.js لتقديم طلبات HTTP التي يعالجها. ويُحدّث هذا النوع ويوسّع نوع Node.js اﻷصلي الذي يتعامل مع الطلبات.
  • النوع Response هو طريقة Express.js لتقديم استجابات HTTP التي يعالجها. ويُحدّث هذا النوع ويوسّع نوع Node.js اﻷصلي الذي يتعامل مع الطلبات.
  • كما يستخدم الحقل NextFunction الذي لا يقل أهمية عن الاثنين السابقين كدالة استدعاء تسمح بتمرير التحكم إلى أية دوال أخرى يضمها الوسيط. وتتشارك جميع البرامج الوسيطة نفس كائنات الطلب والاستجابة قبل أن يُرسل المتحكم الاستجابة إلى صاحب الطلب في النهاية.

الملف app.ts المدخل إلى Node.js

بعد أن وضعنا هيكلية بسيطة للتوجّه في التطبيق، ننتقل إلى تهيئة مدخل entry point إليه، لهذا سننشئ الملف app.ts في المجلد الجذري للمشروع ونبدؤه بالشيفرة التالية:

import express from 'express';
import * as http from 'http';

import * as winston from 'winston';
import * as expressWinston from 'express-winston';
import cors from 'cors';
import {CommonRoutesConfig} from './common/common.routes.config';
import {UsersRoutes} from './users/users.routes.config';
import debug from 'debug';

هناك إدراجان فقط جديدان في هذا الملف هما:

  • http: وهو وحدة برمجية أصلية في Node.js، نحتاجها في تشغيل تطبيق Express.js.
  • body-parser: وهو وسيط يأتي مع Express.js، ويفسّر الطلب (صيغة JSON في حالتنا) قبل وصول التحكم إلى معالج الطلب الذي حددناه.
  • ننتقل بعد إدراج الملفات إلى التصريح عن المتغيرات التي نريد استخدامها:
const app: express.Application = express();
const server: http.Server = http.createServer(app);
const port = 3000;
const routes: Array<CommonRoutesConfig> = [];
const debugLog: debug.IDebugger = debug('app');

تعيد الدالة ()express كائن تطبيق Express.js اﻷساسي الذي نمرره عبر تطبيقنا، من خلال إضافته بدايةً إلى الكائن http.Server (نحتاج إلى تشغيله بعد تهيئة الكائن express.Application الخاص بتطبيقنا)

نترقب اﻵن الطلبات إلى المنفذ 3000 الذي تفهمه TypeScript على أنه من النوع Number بدلًا من المنافذ المعيارية مثل 80 لطلبات HTTP و 443 لطلبات HTTPS التي تُستخدم نمطيًا للاتصال مع الواجهة الأمامية للتطبيق.

لماذا المنفذ 3000؟

لا توجد قاعدة تنص على أن المنفذ يجب أن يكون 3000، وسيُختار رقم المنفذ اعتباطيًا إن لم نخصص واحدًا. لكن الرقم 3000 يستخدم بكثرة في أمثلة توثيق Node.js و Express.js لهذا أكملنا على هذا النحو!

هل يمكن أن تتشارك Node.js المنفذ مع الواجهه اﻷمامية؟

يمكننا أن نشغّل التطبيق محليًا على منفذ مخصص حتى لو أردنا من الواجهة الخلفية أن تستجيب للطلبات على المنافذ المعيارية. يتطلب اﻷمر خادم وكيل عكسي reverse proxy له نطاق رئيسي أو فرعي يستقبل الطلبات على أحد المنفذين 80 أو 443 ثم يعيد توجيهها إلى المنفذ الداخلي 3000.

تتبع المصفوفة routes ملفات التوجيه الخاصة بنا لأغراض التنقيح كما سنرى، ونرى أخيرا كيف ينتهي debugLog بدالة مشابهة للدالة console.log، لكنها أفضل من ناحية إمكانية الضبط الدقيق، إذ تغطي تلقائيًا ما نريد أن ندعو به ملفاتنا أو وحداتنا البرمجية (دعوناه في حالتنا "app" عندما مررناه كنص إلى الدالة البانية ()debug).

أصبحنا اﻵن جاهزين لتهيئة جميع وحدات Express.js الوسيطة والوجهات إلى الواجهة البرمجية:

// JSON نضيف هنا وسيط لتفسير كل الطلبات القادمة بصيغة
app.use(express.json());

// CORS نضيف هنا وسيطًا للسماح بالطلبات مختلطة الأصول
app.use(cors());
//expressWinston نحضّر هنا إعدادات الوحدة الوسيطة المخصصة ﻹدارة التسجيل
//Exprress.js التي تعالجها HTTP والتي تسجّل جميع طلبات
const loggerOptions: expressWinston.LoggerOptions = {
    transports: [new winston.transports.Console()],
    format: winston.format.combine(
        winston.format.json(),
        winston.format.prettyPrint(),
        winston.format.colorize({ all: true })
    ),
};

if (!process.env.DEBUG) {
    loggerOptions.meta = false; // سجل الطلب على سطر واحد إن لم يكن التنقيح مفعّلًا
}

// هيئ المسجل بالإعدادات السابقة
app.use(expressWinston.logger(loggerOptions));

//Express.js إلى مصفوفتنا بعد إرسال كائن UserRoutes نضيف
//كي تضاف الوجهات إلى التطبيق
routes.push(new UsersRoutes(app));

// هذه وجهة بسيطة للتأكد أن كل شيء يعمل كما هو مطلوب
const runningMessage = `Server running at http://localhost:${port}`;
app.get('/', (req: express.Request, res: express.Response) => {
    res.status(200).send(runningMessage)
});

يرتبط expressWinston.logger تلقائيًا بالمكتبة Express.js ويسجل التفاصيل من خلال نفس البنية التحتية التي يستخدمها debug، وذلك لكل طلب مكتمل. وستنسق الخيارات التي مررناها إليه وتلوّن خرج الطرفية التي تعرض السجلات، إضافة إلى عرض سجلات أكثر تفصيلًا (وهو الأمر الافتراضي) عندما نفعّل نمط التنقيح.

وتجدر الملاحظة إلى ضرورة تعريف وجهاتنا بعد إعداد expressWinston.logger.

وأخيرًا نأتي إلى اﻷمر اﻷكثر أهمية:

server.listen(port, () => {
    routes.forEach((route: CommonRoutesConfig) => {
        debugLog(`Routes configured for ${route.getName()}`);
    });
    //console.log الحالة الوحيدة التي لن تحاشى فيها استخدام
    // هو معرّفة متى ينتهي الخادم من عملية اﻹقلاع
    console.log(runningMessage);
});

تشغّل الشيفرة السابقة الخادم فعليًا، وعندما يبدأ تنفيذها يشغّل Node.js توابع الاستدعاء الخاصة بنا والتي تسجّل في وضع التنقيح أسماء كل الوجهات routes التي أعددناها وهي UsersRoutes حتى اللحظة. تنبهنا دوال الاستدعاء بعد ذلك إلى أن الواجه الخلفية جاهزة لاستقبال الطلبات، حتى لو كانت تعمل في وضع الإنتاج.

تحديث الملف package.json لنقل شيفرة TypeScript إلى جافا سكريبت وتشغيل التطبيق

بعد إنجاز البنية اﻷساسية للتطبيق وتحضيره للتشغيل، نحتاج أولًا إلى بعض اﻹعدادات لتمكين نقل transpilation شيفرة TypeScript:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "inlineSourceMap": true
  }
}

نضيف أخيرًا بعض اللمسات النهائية على الملف package.json على هيئة سكربتات:

"scripts": {
    "start": "tsc && node --unhandled-rejections=strict ./dist/app.js",
    "debug": "export DEBUG=* && npm run start",
    "test": "echo \"Error: no test specified\" && exit 1"
},

يعمل السكربت test كملف مؤقت سنستبدله لاحقًا.

تنتمي الوحدة tsc في السكربت start إلى TypeScript، وهو المسؤول عن نقل شيفرة TypeScript إلى جافا سكريبت التي ستظهر في المجلد dist. ثم نشغّل النسخة المبنية من التطبيق باستخدام التعليمة node ./dist/app.js.

نمرر الوسيط unhandled-rejections=strict-- إلى Node.js (حتى في النسخ 16 وأعلى) لإيقاف التنفيذ عند ظهور خطأ غير محسوب في الشيفرة، ويسهّل ذلك معرفة سبب الخطأ وتصحيحه وهذا اﻷسلوب أوضح من الخيار اﻵخر وهو الاعتماد على كائن السجلات expressWinston.errorLogger الذي يزوّدك بقائمة اﻷخطاء بعد توقف المصرّف. ومعنى ذلك أننا سنترك Node.js يعمل على الرغم من وجود خطأ غير محسوب في الشيفرة وقد يسبب ذلك سلوكًا غير متوقع للخادم وظهور أخطاء أخرى قد تكون أكثر تعقيدًا.

يستدعي السكربت debug السكربت start لكنه يعرّف أولًا متغير البيئة DEBUG. ولهذا المتغير تأثير في تمكين جميع عبارات ()debugLog (إضافة إلى تلك التي تقدمها Express.js، والتي تستخدم نفس وحدة التنقيح debug التي نستخدمها) لعرض تفاصيل مفيدة على الطرفية، وإلا ستختفي هذه التفاصيل عند تشغيل الخادم في وضع اﻹنتاج باستخدام التعليمة npm start.

جرّب تنفيذ اﻷمر npm run debug. بنفسك، وقارن نتائج الخرج على الطرفية مع تلك التي تنتج عن تنفيذ npm start.

تلميح: بإمكانك تحديد خرج التنقيح ليعطي فقط عبارات ()debugLog الموجودة في الملف app.ts، وذلك باستخدام DEBUG=app بدلًا من *\=DEBUG. فالوحدة debug مرنة عمومًا، وهذه الميزة ليست استثناءً.

قد يحتاج مستخدمي ويندوز استبدال export بالتعليمة SET لأن export هي الطريقة التي تعمل على لينكس و ماك أو إس. أما إن أردت دعم عدة بيئات تطوير في تطبيقك، جرّب الحزمةcross-env package التي تزوّدك بحلول واضحة لهذه المسألة.

اختبار الواجهة الخلفية

مع تنفيذ أحد اﻷمرين npm run debug أو npm start ستكون الواجهة الخلفية جاهزة لتلقي الطلبات على المنفذ 3000. يمكننا عندها استخدام أحد المكتبات cURL أو Postman أو Insomnia لاختبار الواجهة الخلفية.

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

curl --request GET 'localhost:3000/users/12345'

ستعيد عندها الواجهة الخلفية الاستجابة: GET requested for id 12345.

وعند استخدام POST:

curl --request POST 'localhost:3000/users' \
--data-raw ''

وغيرها من أنواع الطلبات، ستعيد الواجهة الخلفية نفس الاستجابة.

الخلاصة

بدأنا في هذا المقال في إنشاء واجهة برمجية REST بتهيئة المشروع من الصفر ومن ثم دخلنا في أساسيات إطار العمل Express.js. خطونا بعد ذلك أولى خطواتنا في احتراف TypeScript عن طريق بناء نموذج UsersRoutesConfig يرث CommonRoutesConfig وسنعيد استخدام هذا النموذج في الجزء الثاني من هذه السلسلة. أنهينا العمل بعد ذلك بتهيئة ملف المدخل app.ts لاستخدام الوجهات، ومن ثم تهيئة ملف package.json بالسكربتات اللازمة لبناء وتشغيل التطبيق.

وعلى الرغم من استخدام أساسيات الواجهة البرمجية REST مع Express.js و TypeScript في مقالنا، فسوف نركز في المقال التالي على بناء متحكمات مناسبة للموارد والتعرف على نماذج أخرى مثل الخدمات والوحدات الوسيطة والمتحكمات وغيرها من الوحدات البرمجية.

ترجمة -وبتصرف- للمقال Building a Node.js TypeScript REST API Part1 Express.js

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...