بناء واجهة برمجة تطبيقات (API) لتطبيق المهام باستخدام Deno و Oak

دقائق القراءة: 31

في عالم تطوير الويب، تظهر بيئات تشغيل وأطر عمل جديدة باستمرار لتبسيط عملية بناء التطبيقات. Deno، بيئة تشغيل آمنة للـ JavaScript و TypeScript مبنية على V8 و Rust، اكتسبت شعبية واسعة بفضل ميزاتها الأمنية المدمجة وتجربة المطور المحسّنة. في هذا الدليل الشامل، سنخوض رحلة بناء واجهة برمجة تطبيقات (API) لتطبيق المهام (Todo) باستخدام Deno وإطار عمل Oak القوي.

يهدف هذا المقال إلى تزويدك بالمعرفة والخطوات العملية لإنشاء واجهة REST API كاملة لتطبيق المهام. سنركز على الجوانب الأساسية للبنية التحتية للـ API، مع الإشارة إلى أن التعامل مع قواعد البيانات سيتم تغطيته في مقال منفصل لاحقًا. إذا شعرت بالحيرة في أي وقت أو أردت مراجعة الكود المصدري الكامل، يمكنك العثور عليه هنا.

شعار Deno يوضح بيئة التشغيل الجديدة لتطبيقات الويب

ماذا سنتعلم في هذا الدليل؟

  • إنشاء خادم Deno أساسي.
  • بناء خمس واجهات API (مسارات/وحدات تحكم) لتطبيق المهام.
  • تطوير middleware لتسجيل طلبات الـ API في وحدة التحكم.
  • تنفيذ middleware لمعالجة الأخطاء 404 Not Found عند محاولة الوصول إلى مسارات غير موجودة.

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

  • إصدار مثبت من Deno (سوف نوضح خطوات التثبيت).
  • معرفة بسيطة بلغة TypeScript.
  • (اختياري) خبرة سابقة في العمل مع Node.js و Express ستكون مفيدة، ولكنها ليست ضرورية حيث أن هذا الدليل مبسط للغاية.

الخطوة الأولى: تثبيت Deno

لنبدأ بتثبيت Deno. إذا كنت تستخدم نظام macOS، يمكنك فتح الطرفية (Terminal) وكتابة الأمر التالي:

$ brew install deno

لأنظمة التشغيل الأخرى، يرجى زيارة الموقع الرسمي لـ Deno على deno.land، حيث ستجد تعليمات مفصلة وسهلة للتثبيت على جهازك. بعد إتمام التثبيت، أغلق الطرفية وافتح واحدة جديدة، ثم اكتب الأمر التالي للتحقق من الإصدار:

$ deno --version

يجب أن يظهر لك ناتج مشابه لما يلي:

ناتج أمر deno --version يوضح الإصدار المثبت

تهانينا! لقد أكملت الجزء الأول من إعداد بيئة العمل. الآن، دعنا ننتقل إلى بناء واجهة الـ API الخلفية لتطبيق المهام.

إعداد المشروع

قبل البدء، تذكر أن الكود المصدري الكامل لهذا الدليل متاح هنا.

إنشاء هيكل المشروع الأولي

  1. أنشئ مجلدًا جديدًا وسمِّه chapter_1:oak (يمكنك تسميته بأي اسم تفضله).
  2. بعد إنشاء المجلد، انتقل إليه باستخدام الأمر cd في الطرفية.
  3. أنشئ ملفًا باسم server.ts واكتب الكود التالي فيه:
import { Application } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const port: number = 8080;

console.log('running on port ', port);

await app.listen({ port });

تشغيل الخادم الأساسي

لنجرب تشغيل هذا الملف. افتح الطرفية وفي المجلد الرئيسي لمشروعك، اكتب:

$ deno run --allow-net server.ts

سنتحدث لاحقًا عن وظيفة العلامة --allow-net، ولكن في الوقت الحالي، استمر في العمل. يجب أن تحصل على ناتج مشابه لما يلي:

ناتج تشغيل الخادم على Deno يوضح الاستماع على المنفذ 8080

ما قمنا به حتى الآن هو إنشاء خادم يستمع على المنفذ 8080. لا يقوم بالكثير في الوقت الحالي سوى كونه قادرًا على التشغيل على هذا المنفذ.

فهم استيراد الوحدات في Deno وإطار عمل Oak

إذا كنت قد عملت مع JavaScript من قبل، فقد تلاحظ أننا نستورد الحزم بطريقة مختلفة. في Deno، يجب أن نحدد المسار الكامل للحزمة، مثل:

 import { Application } from "https://deno.land/x/oak/mod.ts" ;

عند تشغيل الأمر deno run --allow-net <file_name> في الطرفية، يقوم Deno بالبحث عن جميع الاستيرادات وتثبيتها محليًا في جهازك إذا لم تكن موجودة. في المرة الأولى التي تقوم فيها بالتشغيل، سيذهب إلى الرابط https://deno.land/x/oak/mod.ts ويقوم بتثبيت حزمة Oak. Oak هو في الأساس إطار عمل لـ Deno لكتابة واجهات الـ API، وسيتم تخزينه محليًا في ذاكرة التخزين المؤقت لديك.

في السطر التالي، نقوم بإنشاء مثيل جديد لتطبيقنا:

 const app = new Application();

هذا ينشئ مثيلًا جديدًا لتطبيقنا، وسيكون الأساس لكل ما ستقوم به لاحقًا في هذا الدليل. يمكنك إضافة مسارات (routes) إلى مثيل التطبيق، وإرفاق برمجيات وسيطة (middleware) مثل تسجيل طلبات الـ API، وكتابة معالج لصفحات 404 Not Found، وما إلى ذلك.

ثم نكتب:

 const port: number = 8080 ;
 // const port = 8080; // => يمكن كتابتها هكذا أيضًا

كلا الطريقتين متطابقتان وتؤديان نفس الغرض. الفرق الوحيد هو أن كتابة const port: number = 8080 تخبر TypeScript أن المتغير port من النوع number. إذا حاولت كتابة const port: number = "8080"، فسيؤدي ذلك إلى خطأ في الطرفية، لأن port من النوع number، ولكنك تحاول تعيين قيمة من النوع string له. لمزيد من المعلومات حول أنواع البيانات في TypeScript، يمكنك مراجعة هذا الدليل المبسط حول الأنواع الأساسية.

وفي النهاية، لدينا:

 console .log( 'running on port ' , port);
 await app.listen({ port });

هنا نقوم ببساطة بطباعة رقم المنفذ في وحدة التحكم ونخبر Deno بالاستماع إلى المنفذ 8080. لا يقوم الخادم بالكثير في الوقت الحالي. دعنا نجعله يقوم بشيء بسيط مثل عرض رسالة JSON في متصفحك عند الانتقال إلى http://localhost:8080.

إضافة مسار أساسي (Route)

أضف الكود التالي إلى ملف server.ts الخاص بك:

import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const port: number = 8080;

const router = new Router();
router.get("/", ({ response }: { response: any }) => {
  response.body = {
    message: "hello world",
  };
});

app.use(router.routes());
app.use(router.allowedMethods());

console.log('running on port ', port);
await app.listen({ port });

الجديد هنا هو أننا نستورد الآن Router بالإضافة إلى Application من حزمة Oak في السطر الأول. بعد ذلك، نقوم بما يلي:

 const router = new Router();
 router.get(
  "/",
  ( { response }: { response: any } ) => {
  response.body = {
  message : "hello world" ,
  };
  }
 );
 app.use(router.routes());
 app.use(router.allowedMethods());

نقوم بإنشاء مثيل جديد للموجه (router) باستخدام const router = new Router()، ثم ننشئ مسارًا جديدًا يسمى / وهو من النوع GET. دعنا نفصل هذا الجزء:

router.get(
  "/",
  ( { response }: { response: any } ) => {
  response.body = {
  message : "hello world" ,
  };
  }
 );

الدالة router.get تأخذ معاملين. الأول هو المسار الذي قمنا بتعيينه إلى /، والثاني هو دالة المعالجة. الدالة نفسها تأخذ وسيطًا وهو كائن. ما نقوم به هنا هو تفكيك (destructuring) الكائن والحصول على response فقط. بعد ذلك، نقوم بالتحقق من نوع response بطريقة مشابهة لما فعلناه مع const port: number = 8080;. كل ما نقوم به هو { response }: { response: any }، والذي يخبر TypeScript هنا أن الكائن response الذي قمنا بتفكيكه يمكن أن يكون من النوع any. يساعدك any على تجنب التحقق من النوع في TypeScript. يمكنك قراءة المزيد عنه هنا. ثم كل ما نفعله هو أخذ كائن response هذا وتعيين response.body.message = "hello world";.

response.body = {
  message : "hello world" ,
 };

أخيرًا وليس آخرًا، نضيف هذين السطرين:

app.use(router.routes());
app.use(router.allowedMethods());

يخبر هذا Deno بتضمين جميع المسارات التي يحددها الموجه الخاص بنا (لدينا واحد فقط حاليًا)، ويخبر السطر التالي Deno بالسماح بجميع الأساليب (methods) لهذا المسار/المسارات، مثل GET و POST و PUT و DELETE. والآن انتهينا.

دعنا نشغل هذا ونرى ما لدينا:

$ deno run --allow-net server.ts

الخاصية --allow-net تخبر Deno أن هذا التطبيق يمنح المستخدم الإذن بالوصول إلى محتواه عبر المنفذ المفتوح. الآن افتح متصفحك المفضل وانتقل إلى http://localhost:8080. سترى شيئًا كهذا:

نتيجة تشغيل localhost:8080 في المتصفح

بصراحة، الجزء الأصعب قد انتهى. من الناحية المفاهيمية، لقد وصلنا إلى 60% من الطريق.

صورة توضيحية لتعزيز الفهم

تحسين آلية الاستماع للخادم

شيء أخير قبل أن نبدأ في بناء واجهة API لتطبيق المهام. دعنا نستبدل:

 console .log( 'running on port ' , port);
 await app.listen({ port });

بالكود التالي:

app.addEventListener("listen", ({ secure, hostname, port }) => {
  const protocol = secure ? "https://" : "http://";
  const url = `${protocol}${hostname ?? "localhost"}:${port}`;
  console.log(`Listening on: ${port}`);
});

await app.listen({ port });

الكود السابق لم يكن دقيقًا جدًا، لأننا كنا ببساطة نطبع رسالة في وحدة التحكم ثم ننتظر أن يبدأ التطبيق بالاستماع على منفذ. مع الإصدار الأحدث، ننتظر حتى يبدأ التطبيق بالاستماع على المنفذ port، ويمكننا الاستماع عن طريق إضافة مستمع أحداث (event listener) إلى مثيل app الخاص بنا باستخدام app.addEventListener("listen", ({ secure, hostname, port }) => {}). المعامل الأول هو الحدث الذي نريد الاستماع إليه (وهو listen)، ثم المعامل الثاني هو كائن نقوم بتفكيكه إلى { secure, hostname, port }. secure هو قيمة منطقية (booleanhostname هو سلسلة نصية (string)، و port هو رقم (number). الآن عندما نبدأ تطبيقنا، فإنه سيطبع الرسالة في وحدة التحكم فقط بمجرد أن يبدأ التطبيق فعليًا بالاستماع على المنفذ.

يمكننا أن نذهب خطوة أبعد ونجعلها أكثر حيوية. دعنا نضيف وحدة جديدة في أعلى ملف server.ts:

 import { green, yellow } from "https://deno.land/std@0.53.0/fmt/colors.ts" ;

ثم داخل دالة معالج الأحداث listen، يمكننا استبدال:

 console .log( `Listening on: ${port} ` );

بالكود التالي:

 console .log( ` ${yellow( "Listening on:" )} ${green(url)} ` );

الآن عندما نقوم بتشغيل:

$ deno run --allow-net server.ts

سيعرض هذا في وحدة التحكم لدينا:

ناتج وحدة التحكم الملون بعد تحسين الاستماع

رائع، الآن لدينا وحدة تحكم ملونة. إذا واجهتك أي مشكلة، يمكنك ببساطة الانتقال إلى الكود المصدري لهذا الدليل هنا.

إنشاء مسارات ووحدات تحكم (Controllers) لتطبيق المهام

دعنا ننشئ مسارات (routes) واجهة API لتطبيق المهام. في المجلد الرئيسي لمشروعك، أنشئ مجلدًا جديدًا باسم routes، وداخله أنشئ ملفًا باسم todo.ts. في نفس الوقت، في المجلد الرئيسي، أنشئ مجلدًا جديدًا باسم controllers، وداخله أنشئ ملفًا باسم todo.ts.

تحديد وحدة التحكم (Controller) الأولية

لنبدأ بملف controllers/todo.ts:

export default {
  getAllTodos: () => {},
  createTodo: async () => {},
  getTodoById: () => {},
  updateTodoById: async () => {},
  deleteTodoById: () => {},
};

نحن ببساطة نصدر كائنًا هنا يحتوي على بعض الدوال المسماة التي هي فارغة (في الوقت الحالي).

تحديد المسارات (Routes)

بعد ذلك، انتقل إلى ملف routes/todo.ts واكتب هذا الكود:

import { Router } from "https://deno.land/x/oak/mod.ts";

const router = new Router();

// controller
import todoController from "../controllers/todo.ts";

router
  .get("/todos", todoController.getAllTodos)
  .post("/todos", todoController.createTodo)
  .get("/todos/:id", todoController.getTodoById)
  .put("/todos/:id", todoController.updateTodoById)
  .delete("/todos/:id", todoController.deleteTodoById);

export default router;

قد يبدو هذا مألوفًا للأشخاص الذين عملوا مع Node.js و Express. كل ما نفعله هنا هو استيراد Router من Oak ثم إعداد مثيل جديد للموجه عن طريق const router = new Router();. بعد ذلك، نستورد وحدات التحكم الخاصة بنا عن طريق:

 import todoController from "../controllers/todo.ts" ;

شيء واحد يجب ملاحظته هنا في Deno هو أنه في كل مرة نستورد فيها ملفًا محليًا في مشروع Deno الخاص بنا، يجب علينا توفير امتداد الملف. هذا لأن Deno لا يعرف ما إذا كان الملف المستورد هو ملف .js أو .ts.

بعد ذلك، نقوم ببساطة بتعيين جميع مساراتنا وفقًا لاتفاقيات REST:

router
  .get("/todos", todoController.getAllTodos)
  .post("/todos", todoController.createTodo)
  .get("/todos/:id", todoController.getTodoById)
  .put("/todos/:id", todoController.updateTodoById)
  .delete("/todos/:id", todoController.deleteTodoById);

الكود أعلاه سيترجم إلى تعريف واجهة الـ API الخاصة بنا على النحو التالي:

النوع (TYPE) مسار الـ API (API ROUTE)
GET /todos
GET /todos/:id
POST /todos
PUT /todos/:id
DELETE /todos/:id

وفي النهاية، نقوم ببساطة بتصدير الموجه الخاص بنا عن طريق export default router;. لقد انتهينا من إنشاء هيكل المسارات الخاص بنا. (الآن، كل مسار لا يقوم بأي شيء لأن وحدات التحكم الخاصة بنا فارغة، وسنضيف وظائف إليها قريبًا).

ربط المسارات بمثيل التطبيق

هذه هي القطعة الأخيرة من اللغز قبل أن نبدأ في إضافة وظائف إلى كل وحدة تحكم للمسار. نحتاج إلى ربط هذا الموجه router بمثيل app الخاص بنا. لذا، انتقل إلى ملف server.ts وقم بما يلي:

  1. أضف هذا الاستيراد في الأعلى:
  2. // routes
    import todoRouter from "./routes/todo.ts";
  3. أزل هذا الجزء من الكود:
  4.  const router = new Router();
     router.get(
      "/",
      ( { response }: { response: any } ) => {
      response.body = {
      message : "hello world" ,
      };
      }
     );
     app.use(router.routes());
     app.use(router.allowedMethods());
  5. استبدله بـ:
  6. app.use(todoRouter.routes());
    app.use(todoRouter.allowedMethods());

هذا كل شيء – لقد انتهينا. يجب أن يبدو ملف server.ts الخاص بك الآن هكذا:

import { Application } from "https://deno.land/x/oak/mod.ts";
import { green, yellow } from "https://deno.land/std@0.53.0/fmt/colors.ts";

// routes
import todoRouter from "./routes/todo.ts";

const app = new Application();
const port: number = 8080;

app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());

app.addEventListener("listen", ({ secure, hostname, port }) => {
  const protocol = secure ? "https://" : "http://";
  const url = `${protocol}${hostname ?? "localhost"}:${port}`;
  console.log(
    `${yellow("Listening on:")} ${green(url)}`,
  );
});

await app.listen({ port });

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

إنشاء الواجهات (Interfaces) والبيانات الوهمية (Stubs)

رائع، الآن لدينا مساراتنا بدون أي وظائف في الوقت الحالي. لذا دعنا نضيف هذه الوظائف في وحدات التحكم الخاصة بنا. ولكن قبل أن نفعل ذلك، يجب علينا إنشاء ملفين آخرين (صغيرين).

  1. في المجلد الرئيسي، أنشئ مجلدًا جديدًا باسم interfaces، وداخله أنشئ ملفًا باسم Todo.ts (تأكد من أن Todo مكتوبة بحرف كبير، فهذه مجرد اتفاقيات).
  2. أيضًا في المجلد الرئيسي، أنشئ مجلدًا جديدًا باسم stubs، وداخله أنشئ ملفًا باسم todos.ts.

تعريف واجهة Todo

دعنا ننشئ واجهة في ملف interfaces/Todo.ts. ببساطة أضف الكود التالي:

export default interface Todo {
  id: string,
  todo: string,
  isCompleted: boolean,
}

ما هي الواجهة (interface)؟ أحد الأشياء الأساسية في TypeScript هو التحقق من شكل القيمة. على غرار const port: number = 8080 أو { response }: { response : any }، يمكننا أيضًا التحقق من نوع الكائن. في TypeScript، تملأ الواجهات دور تسمية هذه الأنواع، وهي طريقة قوية لتحديد العقود داخل الكود الخاص بك وكذلك العقود مع الكود خارج مشروعك. إليك مثال آخر على واجهة:

 // لدينا واجهة
 interface LabeledValue {
  label: string ;
 }
 // الوسيط الممرر إلى هذه الدالة labeledObj هو
 // من النوع LabeledValue (واجهة)
 function printLabel ( labeledObj: LabeledValue ) {
  console .log(labeledObj.label);
 }
 let myObj = {label: "Size 10 Object" };
 printLabel(myObj);

نأمل أن يمنحك هذا المثال مزيدًا من الفهم للواجهات. إذا كنت تريد معلومات أكثر تفصيلاً، تحقق من وثائق الواجهات هنا.

إنشاء بيانات المهام الوهمية (Mock Data)

الآن بعد أن أصبحت واجهتنا جاهزة، دعنا ننشئ بعض البيانات الوهمية (بما أنه ليس لدينا قاعدة بيانات فعلية لهذا الدليل). دعنا ننشئ قائمة وهمية من المهام أولاً في ملف stubs/todos.ts. ببساطة أضف ما يلي:

import { v4 } from "https://deno.land/std/uuid/mod.ts";

// interface
import Todo from '../interfaces/Todo.ts';

let todos: Todo[] = [
  {
    id: v4.generate(),
    todo: 'walk dog',
    isCompleted: true,
  },
  {
    id: v4.generate(),
    todo: 'eat food',
    isCompleted: false,
  },
];

export default todos;

هناك شيئان يجب ملاحظتهما هنا: نضيف حزمة جديدة ونستخدم دالتها v4 عن طريق import { v4 } from "https://deno.land/std/uuid/mod.ts";. ثم في كل مرة نستخدم v4.generate()، سيتم إنشاء سلسلة نصية عشوائية جديدة كـ id. لا يمكن أن يكون id رقمًا، بل يجب أن يكون سلسلة نصية فقط، لأننا في واجهة Todo الخاصة بنا قمنا بتعريف id كسلسلة نصية. الشيء الآخر الذي يجب التركيز عليه هنا هو let todos: Todo[] = []. هذا يخبر Deno أن مصفوفة المهام الخاصة بنا من النوع Todo (وهو أمر رائع، فالمترجم الخاص بنا يعرف تلقائيًا أن كل عنصر في مصفوفتنا يمكن أن يحتوي فقط على {id: string, todo: string & isCompleted: boolean} ولن يقبل أي مفتاح آخر). إذا كنت ترغب في معرفة المزيد عن الواجهات في TypeScript، تحقق من هذه الوثائق المفصلة والمذهلة حول الواجهات هنا.

رائع. إذا وصلت إلى هذا الحد، فامنح نفسك مكافأة. عمل جيد للجميع.

صورة معبرة عن التقدم المحرز في المشروع

تطوير وحدات التحكم (Controllers)

في ملف controllers/todo.ts الخاص بك، لدينا الهيكل التالي:

export default {
  getAllTodos: () => {},
  createTodo: async () => {},
  getTodoById: () => {},
  updateTodoById: async () => {},
  deleteTodoById: () => {},
};

1. جلب جميع المهام (getAllTodos)

دعنا نكتب وحدة التحكم لـ getAllTodos:

// stubs
import todos from "../stubs/todos.ts";

export default {
  /**
   * @description Get all todos
   * @route GET /todos
   */
  getAllTodos: ({ response }: { response: any }) => {
    response.status = 200;
    response.body = {
      success: true,
      data: todos,
    };
  },
  createTodo: async () => {},
  getTodoById: () => {},
  updateTodoById: async () => {},
  deleteTodoById: () => {},
};

قبل أن أبدأ في هذا الجزء من الكود، دعني أوضح أن كل وحدة تحكم لها وسيط – دعنا نسميه context. لذا يمكننا تفكيك getAllTodos: (context) => {} إلى:

getAllTodos: ( { request, response, params } ) => {}

وبما أننا نستخدم TypeScript، يجب علينا إضافة التحقق من النوع لجميع هذه المتغيرات:

getAllTodos: (
  { request, response, params }: {
  request: any,
  response: any,
  params: { id: string },
  },
 ) => {}

لذا فقد أضفنا التحقق من النوع لجميع المتغيرات الثلاثة { request, response, params }:

  • request هو ما يرسله المستخدم إلينا (معلومات مثل الرؤوس headers وبيانات JSON).
  • response هو ما نرسله للمستخدم في استجابة الـ API.
  • params هو ما نحدده في مسارات الموجه (router routes) الخاص بنا، أي:
.get( "/todos/:id" , ( { params}: { params: { id: string } } ) => {})

إذن، :id في /todos/:id هو المعامل (param). المعاملات هي طريقة للحصول على معلومات من عنوان URL. في هذا المثال، نعلم أن لدينا /:id. لذلك عندما يحاول المستخدم الوصول إلى واجهة الـ API هذه (أي /todos/756)، فإن 756 هو في الأساس المعامل :id. وبما أنه موجود في عنوان URL، نعلم أنه من النوع string.

الآن بعد أن حددنا تعريفاتنا الأساسية، دعنا نعود إلى وحدة تحكم المهام:

// stubs
import todos from "../stubs/todos.ts";

export default {
  /**
   * @description Get all todos
   * @route GET /todos
   */
  getAllTodos: ({ response }: { response: any }) => {
    response.status = 200;
    response.body = {
      success: true,
      data: todos,
    };
  },
  createTodo: async () => {},
  getTodoById: () => {},
  updateTodoById: async () => {},
  deleteTodoById: () => {},
};

لوحدة التحكم getAllTodos، نحتاج فقط إلى response. إذا تذكرت، response هو ما نحتاجه لإرسال البيانات مرة أخرى إلى المستخدم. بالنسبة للأشخاص القادمين من خلفية Node.js و Express، هناك شيء كبير يختلف هنا وهو أننا لا نحتاج إلى إرجاع كائن الاستجابة return. يقوم Deno بذلك تلقائيًا لنا. كل ما علينا فعله هو تعيين response.status، والذي في هذه الحالة هو 200. المزيد عن حالات الاستجابة هنا. الشيء الآخر الذي نحدده هو response.body، والذي في هذه الحالة هو كائن:

{ success: true , data: todos }

سأقوم الآن بتشغيل الخادم الخاص بي:

$ deno run --allow-net server.ts

مراجعة: الخاصية --allow-net تخبر Deno أن هذا التطبيق يمنح المستخدم الإذن بالوصول إلى محتواه عبر المنفذ المفتوح. بمجرد تشغيل الخادم، يمكنك الوصول إلى واجهة GET /todos API. أنا أستخدم Postman، وهو إضافة لمتصفح Google Chrome ويمكن تنزيله هنا. يمكنك استخدام أي عميل REST تفضله. أنا أحب استخدام Postman لأنه سهل الاستخدام للغاية. في Postman، افتح علامة تبويب جديدة. اضبط الطلب على النوع GET وفي شريط URL اكتب http://localhost:8080/todos. اضغط على Send وهذا ما ستراه:

استجابة API لـ GET /todos تعرض قائمة المهام

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

2. إضافة مهمة جديدة (createTodo)

دعنا ننتقل إلى وحدة التحكم التالية:

import { v4 } from "https://deno.land/std/uuid/mod.ts";

// interfaces
import Todo from "../interfaces/Todo.ts";

// stubs
import todos from "../stubs/todos.ts";

export default {
  getAllTodos: () => {},
  /**
   * @description Add a new todo
   * @route POST /todos
   */
  createTodo: async (
    { request, response }: { request: any; response: any },
  ) => {
    const body = await request.body();

    if (!request.hasBody) {
      response.status = 400;
      response.body = {
        success: false,
        message: "No data provided",
      };
      return;
    }

    // if everything is fine then perform
    // operation and return todos with the
    // new data added.
    let newTodo: Todo = {
      id: v4.generate(),
      todo: body.value.todo,
      isCompleted: false,
    };

    let data = [...todos, newTodo];
    response.body = {
      success: true,
      data,
    };
  },
  getTodoById: () => {},
  updateTodoById: async () => {},
  deleteTodoById: () => {},
};

بما أننا سنضيف مهمة جديدة إلى قائمتنا، فقد قمت باستيراد وحدتين جديدتين في ملف وحدة التحكم:

  • import { v4 } from https://deno.land/std/uuid/mod.ts;: ستُستخدم هذه لإنشاء معرف فريد جديد للمهمة التي يتم إنشاؤها.
  • import Todo from "../interfaces/Todo.ts";: ستُستخدم هذه لضمان أن المهمة الجديدة التي يتم إنشاؤها تتبع نفس الهيكل المحدد في الواجهة.

وحدة التحكم createTodo هي دالة غير متزامنة (async)، مما يعني وجود بعض الوعود (promises) المستخدمة داخلها. دعنا نقسمها إلى أجزاء أصغر:

 const body = await request.body();
 if (!request.hasBody) {
  response.status = 400 ;
  response.body = {
  success: false ,
  message: "No data provided" ,
  };
  return ;
 }

أولاً، نحصل على محتوى جسم JSON الذي أرسله المستخدم إلينا. ثم نستخدم دالة Oak المدمجة request.hasBody للتحقق مما إذا كان المستخدم قد أرسل أي محتوى على الإطلاق. إذا لم يكن كذلك، يمكننا تنفيذ الكتلة if (!request.hasBody) {}. داخل هذه الكتلة، نقوم بتعيين الحالة إلى 400 (الرمز 400 يعني أن المستخدم قام بشيء غير متوقع) ويتم تعيين جسم الاستجابة إلى {success: false, message: "No data provided"}. ثم نضيف ببساطة return; لضمان عدم تنفيذ أي كود آخر أسفل هذه الكتلة.

بعد ذلك نقوم بما يلي:

 // إذا كان كل شيء على ما يرام، فقم بتنفيذ
 // العملية وأعد المهام مع
 // البيانات الجديدة المضافة.
 let newTodo: Todo = {
  id : v4.generate(),
  todo : body.value.todo,
  isCompleted : false ,
 };
 let data = [...todos, newTodo];
 response.body = {
  success : true ,
  data,
 };

نقوم بإنشاء مهمة جديدة عن طريق:

 let newTodo: Todo = {
  id : v4.generate(),
  todo : body.value.todo,
  isCompleted : false ,
 };

يضمن let newTodo: Todo = {} أن newTodo يتبع نفس هيكل بقية المهام. ثم نقوم بتعيين معرف عشوائي باستخدام v4.generate()، وتعيين todo إلى body.value.todo و isCompleted إلى false. الشيء الذي يجب ملاحظته هنا هو أن جميع البيانات التي يرسلها المستخدم إلينا يمكننا الوصول إليها من body.value في Oak. بعد ذلك نقوم بما يلي:

 let data = [...todos, newTodo];
 response.body = {
  success : true ,
  data,
 };

نلحق newTodo بقائمة المهام الحالية لدينا ونقوم ببساطة بتعيين جسم الاستجابة إلى {success: true & data: data}. لقد انتهينا من وحدة التحكم هذه أيضًا. دعنا نعيد تشغيل الخادم الخاص بنا:

$ deno run --allow-net server.ts

في Postman الخاص بي، أفتح علامة تبويب جديدة. أضبط الطلب على النوع POST وفي شريط URL أكتب http://localhost:8080/todos. ثم أضغط على Send وهذا ما ستراه:

إرسال طلب POST فارغ والحصول على خطأ 400

أرسلت طلبًا فارغًا وحصلت على رمز خطأ 400 مع رسالة خطأ. ثم أرسل بعض المحتوى في جسم حمولة الطلب وأحاول مرة أخرى:

طلب POST ناجح مع محتوى { todo: 'eat a lamma' }

رائع، POST /todos مع محتوى الجسم { todo: "eat a lamma" } ناجح، ويمكننا رؤية المحتوى ملحقًا بقائمة المهام الحالية لدينا. يمكننا أن نرى أن واجهة الـ API الخاصة بنا تعمل كما هو متوقع. تم إنجاز واجهتي API، وبقيت ثلاث أخرى. لقد اقتربنا من النهاية. معظم العمل الشاق قد انتهى.

3. جلب مهمة بواسطة المعرف (getTodoById)

دعنا ننتقل إلى واجهة الـ API الثالثة:

import { v4 } from "https://deno.land/std/uuid/mod.ts";

// interfaces
import Todo from "../interfaces/Todo.ts";

// stubs
import todos from "../stubs/todos.ts";

export default {
  getAllTodos: () => {},
  createTodo: async () => {},
  /**
   * @description Get todo by id
   * @route GET todos/:id
   */
  getTodoById: (
    { params, response }: { params: { id: string }; response: any },
  ) => {
    const todo: Todo | undefined = todos.find((t) => {
      return t.id === params.id;
    });

    if (!todo) {
      response.status = 404;
      response.body = {
        success: false,
        message: "No todo found",
      };
      return;
    }

    // If todo is found
    response.status = 200;
    response.body = {
      success: true,
      data: todo,
    };
  },
  updateTodoById: async () => {},
  deleteTodoById: () => {},
};

دعنا نتحدث عن وحدة التحكم الخاصة بنا لـ GET todos/:id. ستجلب لنا هذه المهمة بواسطة المعرف. دعنا نقسم هذا إلى أجزاء أصغر ونناقشه:

 const todo: Todo | undefined = todos.find(
  ( t ) => t.id === params.id);
 if (!todo) {
  response.status = 404 ;
  response.body = {
  success : false ,
  message : "No todo found" ,
  };
  return ;
 }

في الجزء الأول، نحدد متغير const todo جديدًا ونعين نوعه إما Todo أو undefined. لذا، سيكون todo إما كائنًا له شكل واجهة Todo أو سيكون undefined – لا يمكن أن يكون أي شيء آخر. ثم نستخدم todos.find((t) => t.id === params.id); للبحث عن المهمة (todo) بالمعرف المقدم في params.id. إذا تطابق، نحصل على Todo بالشكل todo، وإلا undefined. إذا كان todo غير معرف (undefined)، فهذا يعني أن كتلة if هذه ستعمل:

 if (!todo) {
  response.status = 404 ;
  response.body = {
  success : false ,
  message : "No todo found" ,
  };
  return ;
 }

هنا نقوم ببساطة بتعيين الحالة إلى 404، مما يعني “غير موجود” (not found)، بالإضافة إلى استجابتنا الفاشلة القياسية أو { status, message }. رائع، أليس كذلك؟

بعد ذلك، نقوم ببساطة بما يلي:

 // إذا تم العثور على المهمة
 response.status = 200 ;
 response.body = {
  success : true ,
  data : todo,
 };

نقوم بتعيين استجابة نجاح 200، وفي جسم الاستجابة الخاص بنا نحدد success: true & data: todo. دعنا نشغل هذا في Postman الخاص بنا. دعنا نعيد تشغيل الخادم الخاص بنا:

$ deno run --allow-net server.ts

في Postman الخاص بي، أفتح علامة تبويب جديدة. أضبط الطلب على النوع GET وفي شريط URL أكتب http://localhost:8080/todos/:id، ثم أضغط على Send. بما أننا نولد المعرفات عشوائيًا، قم أولاً بجلب جميع المهام عن طريق استدعاء واجهة GET /todos API. ثم من أي مهمة، احصل على أحد معرفاتها لاختبار واجهة الـ API التي تم إنشاؤها حديثًا. في كل مرة تعيد تشغيل تطبيق Deno هذا، سيتم إنشاء معرفات جديدة. دعنا نرى:

حالة 404، لم يتم العثور على سجل
تم توفير معرف معروف وعاد بالمهمة المرتبطة به مع حالة 200

إذا كنت بحاجة إلى الرجوع إلى الكود المصدري الأصلي لهذا الدليل، انتقل هنا. عظيم، تم إنجاز 3 واجهات API، وبقيت اثنتان أخريان.

4. تحديث مهمة بواسطة المعرف (updateTodoById)

import { v4 } from "https://deno.land/std/uuid/mod.ts";

// interfaces
import Todo from "../interfaces/Todo.ts";

// stubs
import todos from "../stubs/todos.ts";

export default {
  getAllTodos: () => {},
  createTodo: async () => {},
  getTodoById: () => {},
  /**
   * @description Update todo by id
   * @route PUT todos/:id
   */
  updateTodoById: async (
    { params, request, response }: {
      params: { id: string };
      request: any;
      response: any;
    },
  ) => {
    const todo: Todo | undefined = todos.find((t) => t.id === params.id);

    if (!todo) {
      response.status = 404;
      response.body = {
        success: false,
        message: "No todo found",
      };
      return;
    }

    // if todo found then update todo
    const body = await request.body();
    const updatedData: { todo?: string; isCompleted?: boolean } = body.value;

    let newTodos = todos.map((t) => {
      return t.id === params.id ? { ...t, ...updatedData } : t;
    });

    response.status = 200;
    response.body = {
      success: true,
      data: newTodos,
    };
  },
  deleteTodoById: () => {},
};

دعنا نتحدث عن وحدة التحكم الخاصة بنا لـ PUT todos/:id. ستحدث هذه المهمة بواسطة المعرف. دعنا نقسم هذا إلى أجزاء أصغر:

 const todo: Todo | undefined = todos.find(
  ( t ) => t.id === params.id);
 if (!todo) {
  response.status = 404 ;
  response.body = {
  success : false ,
  message : "No todo found" ,
  };
  return ;
 }

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

بعد ذلك نقوم بما يلي:

 // إذا تم العثور على المهمة، فقم بتحديثها
 const body = await request.body();
 const updatedData: { todo?: string; isCompleted?: boolean } = body.value;
 let newTodos = todos.map(
  ( t ) => {
  return t.id === params.id ? { ...t, ...updatedData } : t;
  }
 );
 response.status = 200 ;
 response.body = {
  success : true ,
  data : newTodos,
 };

الجزء الذي أريد التحدث عنه هنا هو التالي:

 const updatedData: { todo?: string; isCompleted?: boolean } = body.value;
 let newTodos = todos.map(
  ( t ) => {
  return t.id === params.id ? { ...t, ...updatedData } : t;
  }
 );

أولاً، نقوم بـ const updatedData = body.value ثم نضيف التحقق من النوع إلى updatedData على النحو التالي:

updatedData: { todo?: string; isCompleted?: boolean }

يخبر هذا الجزء من الكود TypeScript أن updatedData هو كائن يمكن أن يحتوي على todo: string و isCompleted: boolean أو لا يحتوي عليهما (علامة الاستفهام ? تجعل الخاصية اختيارية). ثم نقوم ببساطة بتكرار (map) على جميع المهام على النحو التالي:

 let newTodos = todos.map(
  ( t ) => {
  return t.id === params.id ? { ...t, ...updatedData } : t;
  }
 );

وحيث يتطابق params.id مع t.id، فإننا ببساطة نلحق كل شيء بهذا الكائن الذي نحصل عليه من المستخدم. لقد انتهينا من واجهة الـ API هذه أيضًا. دعنا نعيد تشغيل الخادم الخاص بنا:

$ deno run --allow-net server.ts

افتح علامة تبويب جديدة في Postman. اضبط الطلب على PUT وفي شريط URL اكتب http://localhost:8080/todos/:id، ثم اضغط على Send. بما أننا نولد المعرفات عشوائيًا، قم أولاً بجلب جميع المهام عن طريق استدعاء واجهة GET /todos API. ثم من أي مهمة، احصل على أحد معرفاتها لاختبار واجهة الـ API التي تم إنشاؤها حديثًا. في كل مرة تعيد تشغيل تطبيق Deno هذا، سيتم إنشاء معرفات جديدة.

حالة 404، لم يتم العثور على مهمة
تم توفير معرف معروف، وتحديث محتوى المهمة في الجسم. وعادت المهمة المحدثة مع جميع المهام الأخرى.

هذا مذهل – تم إنجاز أربع واجهات API وبقيت واحدة فقط.

5. حذف مهمة بواسطة المعرف (deleteTodoById)

import { v4 } from "https://deno.land/std/uuid/mod.ts";

// interfaces
import Todo from "../interfaces/Todo.ts";

// stubs
import todos from "../stubs/todos.ts";

export default {
  getAllTodos: () => {},
  createTodo: async () => {},
  getTodoById: () => {},
  updateTodoById: async () => {},
  /**
   * @description Delete todo by id
   * @route DELETE todos/:id
   */
  deleteTodoById: (
    { params, response }: { params: { id: string }; response: any },
  ) => {
    const allTodos = todos.filter((t) => t.id !== params.id);

    // remove the todo w.r.t id and return
    // remaining todos
    response.status = 200;
    response.body = {
      success: true,
      data: allTodos,
    };
  },
};

دعنا نتحدث عن وحدة التحكم الخاصة بنا لـ Delete todos/:id، ستحذف هذه المهمة بواسطة المعرف. نقوم ببساطة بتطبيق عامل تصفية (filter) على جميع المهام:

 const allTodos = todos.filter(
  ( t ) => t.id !== params.id);

نحذف todo.id الذي يتطابق مع params.id ونعيد البقية. ثم نقوم بما يلي:

 // إزالة المهمة المتعلقة بالمعرف وإرجاع
 // المهام المتبقية
 response.status = 200 ;
 response.body = {
  success : true ,
  data : allTodos,
 };

ببساطة، نرجع جميع المهام المتبقية التي لا تحتوي على نفس todo.id. دعنا نعيد تشغيل الخادم الخاص بنا:

$ deno run --allow-net server.ts

افتح علامة تبويب جديدة في Postman. هذه المرة، اضبط الطلب على DELETE وفي شريط URL اكتب http://localhost:8080/todos/:id واضغط على Send. بما أننا نولد المعرفات عشوائيًا، قم أولاً بجلب جميع المهام عن طريق استدعاء واجهة GET /todos API. ثم من أي مهمة، احصل على أحد معرفاتها لاختبار واجهة الـ API التي تم إنشاؤها حديثًا. في كل مرة تعيد تشغيل تطبيق Deno هذا، سيتم إنشاء معرفات جديدة.

استجابة API لـ DELETE /todos/:id بعد حذف مهمة

بهذا، نكون قد انتهينا من جميع واجهات الـ API الخمس.

صورة توضيحية لانتهاء مرحلة هامة من المشروع

إضافة البرمجيات الوسيطة (Middlewares)

الآن لم يتبق لدينا سوى شيئين:

  1. إضافة middleware للمسارات غير الموجودة (not found route) بحيث عندما يحاول المستخدم الوصول إلى مسار غير معروف، فإنه يعطي خطأ.
  2. إضافة API logger يسجل وقت الاستجابة الذي استغرقه إرجاع البيانات من نقطة نهاية API في وحدة التحكم.

إنشاء برمجية وسيطة للمسارات غير الموجودة (404 Not Found)

في المجلد الرئيسي لمشروعك، أنشئ مجلدًا جديدًا باسم middlewares. داخل هذا المجلد، أنشئ ملفًا باسم notFound.ts وداخله أضف هذا الكود:

export default ({ response }: { response: any }) => {
  response.status = 404;
  response.body = {
    success: false,
    message: "404 - Not found.",
  };
};

هنا لا نفعل أي شيء جديد – إنه مشابه جدًا لهيكل وحدات التحكم الخاصة بنا. فقط نرجع حالة 404 (التي تعني “غير موجود”) جنبًا إلى جنب مع كائن JSON لـ { success, message }.

بعد ذلك، انتقل إلى ملف server.ts الخاص بك وأضف المحتوى التالي:

  1. أضف هذا الاستيراد في الأعلى:
  2. // not found
    import notFound from './middlewares/notFound.ts';
  3. ثم أسفل app.use(todoRouter.allowedMethods()) مباشرة، أضف هذا السطر هكذا:
  4. app.use(todoRouter.routes());
    app.use(todoRouter.allowedMethods());
    
    // 404 page
    app.use(notFound);

ترتيب التنفيذ مهم هنا: في كل مرة نحاول فيها الوصول إلى نقطة نهاية API، سيتم أولاً مطابقة/التحقق من المسارات من todoRouter الخاص بنا. إذا لم يتم العثور على أي منها، فسيتم تنفيذ app.use(notFound);. دعنا نرى ما إذا كان هذا يعمل. أعد تشغيل الخادم:

$ deno run --allow-net server.ts

افتح علامة تبويب جديدة في Postman. اضبط الطلب على GET وفي شريط URL اكتب http://localhost:8080/something-unknown، ثم اضغط على Send.

استجابة 404 لعنوان URL غير معروف

لدينا الآن middleware للمسارات التي وضعناها في نهاية مساراتنا في server.ts كـ app.use(notFound);. إذا لم يتطابق أي مسار مع هذا الـ middleware، فسيتم تنفيذه وإرجاع رمز حالة 404 (الذي يعني “غير موجود”). ثم نرسل ببساطة رسالة استجابة كالمعتاد وهي {success, message}. نصيحة احترافية: لقد قررنا أن {success, message} هو ما نرجعه في سيناريوهات الفشل وأن {success, data} هو ما نرجعه للمستخدم في سيناريوهات النجاح. لذلك يمكننا حتى جعل هذه الكائنات/الأشكال واجهات وإضافتها إلى مشروعنا لضمان الاتساق والتحقق الآمن من النوع.

رائع، الآن انتهينا من أحد برمجياتنا الوسيطة – دعنا نضيف البرمجية الوسيطة الأخرى لتسجيل واجهات الـ API الخاصة بنا في وحدة التحكم. تذكير: إذا واجهتك أي مشكلة، يمكنك استخدام الكود المصدري هنا.

تسجيل طلبات الـ API في وحدة التحكم

في مجلد middlewares الخاص بك، أنشئ ملفًا جديدًا باسم logger.ts وأدخل الكود التالي:

import {
  green,
  cyan,
  white,
  bgRed,
} from "https://deno.land/std@0.53.0/fmt/colors.ts";

const X_RESPONSE_TIME: string = "X-Response-Time";

export default {
  logger: async (
    { response, request }: { response: any; request: any },
    next: Function,
  ) => {
    await next();
    const responseTime = response.headers.get(X_RESPONSE_TIME);
    console.log(`${green(request.method)} ${cyan(request.url.pathname)}`);
    console.log(`${bgRed(white(String(responseTime)))}`);
  },
  responseTime: async (
    { response }: { response: any },
    next: Function,
  ) => {
    const start = Date.now();
    await next();
    const ms: number = Date.now() - start;
    response.headers.set(X_RESPONSE_TIME, `${ms}ms`);
  },
};

في ملف server.ts الخاص بك، أضف هذا الكود:

  1. استورد هذا في الأعلى:
  2. // logger
    import logger from './middlewares/logger.ts';
  3. فوق كود todoRouter مباشرة، أضف هذه البرمجيات الوسيطة هكذا:
  4. // order of execution is important;
    app.use(logger.logger);
    app.use(logger.responseTime);
    app.use(todoRouter.routes());
    app.use(todoRouter.allowedMethods());

الآن دعنا نناقش ما فعلناه للتو. دعنا نتحدث عن ملف logger.ts ونقسمه إلى أجزاء:

 import { green, cyan, white, bgRed, } from "https://deno.land/std@0.53.0/fmt/colors.ts" ;

أقوم باستيراد بعض ألوان وحدة التحكم وألوان خلفية وحدة التحكم التي أرغب في استخدامها في تسجيل واجهة الـ API. هذا مشابه لما فعلناه في eventListener في ملف server.ts. سنستخدم الألوان في وحدة التحكم لتسجيل طلبات الـ API.

بعد ذلك، أقوم بتعيين const X_RESPONSE_TIME: string = "X-Response-Time";. هذا هو الرأس (header) الذي سنقوم بحقنه في طلبات الـ API عندما تصل إلى خادمنا. أسمي هذا X_RESPONSE_TIME وقيمته هي X-Response-Time. سأوضح استخدامه بعد قليل.

بعد ذلك، نقوم ببساطة بتصدير كائن كهذا:

export default {
  logger: async ({ response, request }, next) {},
  responseTime: async ({ response }, next) {},
};

ثم نستخدمه ببساطة داخل ملف server.ts الخاص بنا هكذا:

// order of execution is important;
app.use(logger.logger);
app.use(logger.responseTime);

دعنا الآن نناقش ما يحدث في كود middleware الخاص بالتسجيل ونناقش أسلوب تنفيذه باستخدام next():

مخطط يوضح ترتيب تنفيذ البرمجية الوسيطة عند استدعاء GET /todos API

الفرق الوحيد هنا وفي وحدات التحكم التي كانت لدينا من قبل هو استخدام دالة next(). تساعدنا هذه الدالة على الانتقال من وحدة تحكم إلى أخرى كما هو موضح في الصورة أعلاه. لذا في:

export default {
  logger: async (
    { response, request }: { response: any; request: any },
    next: Function,
  ) => {
    await next();
    const responseTime = response.headers.get(X_RESPONSE_TIME);
    console.log(`${green(request.method)} ${cyan(request.url.pathname)}`);
    console.log(`${bgRed(white(String(responseTime)))}`);
  },
  responseTime: async (
    { response }: { response: any },
    next: Function,
  ) => {
    const start = Date.now();
    await next();
    const ms: number = Date.now() - start;
    response.headers.set(X_RESPONSE_TIME, `${ms}ms`);
  },
};

تذكر أن هذا ما لدينا في ملف server.ts الخاص بنا:

// order of execution is important;
app.use(logger.logger);
app.use(logger.responseTime);
app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());

ترتيب التنفيذ هو كما يلي:

  1. logger.logger middleware
  2. logger.responseTime middleware
  3. وحدة تحكم todoRouter (أي مسار يستدعيه المستخدم، لغرض الشرح أفترض أن المستخدم استدعى GET /todos API للحصول على جميع المهام).

لذا، سيتم أولاً تنفيذ logger.logger middleware وهو هذا:

logger: async (
  { response, request }: { response: any; request: any },
  next: Function,
) => {
  await next();
  const responseTime = response.headers.get(X_RESPONSE_TIME);
  console.log(`${green(request.method)} ${cyan(request.url.pathname)}`);
  console.log(`${bgRed(white(String(responseTime)))}`);
},

سيدخل داخل هذه الدالة وبمجرد قراءته await next()، فإنه يقفز بسرعة إلى الـ middleware التالي وهو responseTime:

مخطط يوضح ترتيب تنفيذ البرمجية الوسيطة عند استدعاء GET /todos API (مكرر)

داخل responseTime، ينفذ سطرين فقط وهما (انظر ترتيب التنفيذ 2 في الصورة أعلاه):

const start = Date.now();
await next();

قبل القفز إلى وحدة التحكم getAllTodos. بمجرد دخوله إلى getAllTodos، سيقوم بتشغيل الكود بأكمله داخل وحدة التحكم هذه. وبما أننا لا نستخدم next() في وحدة التحكم هذه، فإنه سيعيد تدفق المنطق ببساطة إلى وحدة التحكم responseTime. هناك سيقوم بتشغيل ما يلي:

const ms: number = Date.now() - start;
response.headers.set(X_RESPONSE_TIME, `${ms}ms`)

مع الأخذ في الاعتبار ترتيب التنفيذ وهو 2, 3, 4 (انظر الصورة أعلاه). هذا ما يحدث:

نلتقط البيانات في ms عن طريق const start = Date.now();. ثم نستدعي next() على الفور، والذي ينتقل إلى وحدة التحكم getAllTodos ويشغل الكود بأكمله. ثم يعود إلى وحدة التحكم responseTime. ثم نطرح تاريخ البدء هذا من التاريخ الحالي عن طريق const ms: number = Date.now() - start;. هنا سيعيد رقمًا وهو في الأساس الفرق بالمللي ثانية الذي سيخبرنا بالوقت الذي استغرقه Deno لتنفيذ وحدة التحكم getAllTodos الخاصة بنا.

مخطط يوضح ترتيب تنفيذ البرمجية الوسيطة عند استدعاء GET /todos API (مكرر)

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

response.headers.set(X_RESPONSE_TIME, ` ${ms} ms` )

والذي يقوم ببساطة بتعيين قيمة الرأس X-Response-Time إلى المللي ثانية التي استغرقها Deno لتنفيذ واجهة الـ API الخاصة بنا. ثم من ترتيب التنفيذ 4 نعود إلى ترتيب التنفيذ 5 (انظر الصورة أعلاه للمرجع). هنا نقوم ببساطة بما يلي:

const responseTime = response.headers.get(X_RESPONSE_TIME);
console.log(`${green(request.method)} ${cyan(request.url.pathname)}`);
console.log(`${bgRed(white(String(responseTime)))}`);

نحصل على الوقت الذي مررناه في X-Response-Time. ثم نأخذ هذا الوقت ونطبعه ببساطة بشكل ملون في وحدة التحكم. يخبرنا request.method بالأسلوب المستخدم لاستدعاء واجهة الـ API الخاصة بنا، أي GET، PUT، إلخ، بينما سيخبر request.url.pathname واجهة الـ API بالمسار الذي استخدمه المستخدم، أي /todos.

دعنا نرى ما إذا كان هذا يعمل. أعد تشغيل الخادم:

$ deno run --allow-net server.ts

افتح علامة تبويب جديدة في Postman. اضبط الطلب على GET، اكتب http://localhost:8080/todos، واضغط على Send.

طلب GET /todos في Postman

اضغط على واجهة الـ API عدة مرات في Postman. ثم عندما تعود إلى وحدة التحكم، يجب أن ترى شيئًا كهذا:

سجلات API الملونة في وحدة التحكم

هذا كل شيء – لقد انتهينا. إذا كنت لا تزال تشعر بأنك عالق، ألق نظرة على الكود المصدري الكامل لهذا الدليل هنا.

الخلاصة التقنية

لقد قمنا في هذا الدليل ببناء واجهة برمجة تطبيقات RESTful API كاملة لتطبيق المهام باستخدام Deno وإطار عمل Oak. استعرضنا كيفية إعداد بيئة Deno، وتحديد الواجهات باستخدام TypeScript لضمان سلامة البيانات، وإنشاء وحدات تحكم قوية للتعامل مع عمليات CRUD (إنشاء، قراءة، تحديث، حذف). كما قمنا بتطبيق برمجيات وسيطة متقدمة لمعالجة الأخطاء 404 وتسجيل طلبات الـ API بشكل فعال، مما يعزز من قابلية صيانة التطبيق وقابلية التوسع. يبرهن هذا المشروع على قوة Deno في توفير بيئة تطوير آمنة وفعالة، ومرونة Oak في بناء تطبيقات الويب الحديثة. إن استخدام TypeScript من البداية يضيف طبقة قوية من التحقق من النوع، مما يقلل من الأخطاء ويزيد من جودة الكود.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *