بناء واجهة برمجة تطبيقات (API) لتطبيق إدارة المهام باستخدام Deno و Oak: دليل شامل
مرحباً بكم في منصة قيد! بصفتي مطور JavaScript/Node.js، أكنّ إعجاباً كبيراً (بل حباً وتقديراً) لـ Deno منذ الإعلان عنه، وكنت أتطلع دائماً للتعمق في استكشافه. يركز هذا الدليل الشامل على بناء مجموعة من واجهات برمجة التطبيقات (REST APIs) لتطبيق إدارة المهام (Todo). تجدر الإشارة إلى أننا لن نتطرق إلى قواعد البيانات في هذا المقال؛ سيتم تغطية ذلك في مقال لاحق. إذا شعرت بالضياع في أي نقطة أو أردت مراجعة الكود المصدري كاملاً لهذا الدليل، يمكنك العثور عليه هنا: Chapter 1: Oak.

الصورة من Bernard de Clerk عبر Unsplash.
ماذا سنتعلم في هذا الدليل؟
سنتناول في هذا الدليل النقاط الأساسية التالية لتمكينك من بناء واجهة API متكاملة:
- إنشاء خادم
Denoأساسي. - تطوير 5 واجهات
APIرئيسية (مسارات/وحدات تحكم) لتطبيق المهام. - بناء وسيط (
middleware) لتسجيل طلباتAPIفي وحدة التحكم. - إنشاء وسيط (
middleware) لمعالجة الأخطاء404 Not Foundعند محاولة المستخدم الوصول إلى مسار غير موجود.
المتطلبات الأساسية
لتحقيق أقصى استفادة من هذا الدليل، ستحتاج إلى:
- إصدار مثبت من
Deno(لا تقلق، سأرشدك خلال عملية التثبيت). - معرفة بسيطة بـ
TypeScript. - (اختياري) خبرة سابقة في العمل مع
Node.jsوExpressستكون مفيدة، لكنها ليست ضرورية، فالدليل مبسط للغاية.
لنبدأ: تثبيت Deno
الخطوة الأولى والأهم هي تثبيت Deno. إذا كنت تستخدم نظام macOS، يمكنك استخدام brew. افتح الطرفية (terminal) واكتب الأمر التالي:
$ brew install deno
إذا كنت تستخدم نظام تشغيل مختلفاً، توجه إلى صفحة التثبيت الرسمية على deno.land. ستجد هناك العديد من الطرق السهلة لتثبيته على جهازك.
بعد الانتهاء من التثبيت، أغلق الطرفية وافتح واحدة جديدة، ثم اكتب:
$ deno --version
يجب أن يظهر لك إخراج مشابه لهذا:

ممتاز! بهذا نكون قد أنجزنا حوالي 10% من هذا الدليل. لننتقل الآن إلى إنشاء الواجهة الخلفية (backend API) لتطبيق المهام الخاص بنا.
إعداد المشروع
قبل المضي قدماً، تذكر أن الكود المصدري الكامل لهذا الدليل متاح هنا: Chapter 1: Oak.
لنبدأ بإنشاء مجلد جديد ونسميه chapter_1:oak (يمكنك تسميته بأي اسم تفضله). بمجرد إنشاء المجلد، انتقل إليه باستخدام الأمر cd في الطرفية. ثم، أنشئ ملفاً باسم 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، ولكن في الوقت الحالي، اتبع الخطوات فقط. يجب أن تحصل على إخراج مشابه لهذا:

فهم الكود الأولي
ما قمنا به حتى الآن هو إنشاء خادم يستمع على المنفذ 8080. لا يقوم بالكثير في الوقت الحالي سوى الاستماع على هذا المنفذ. إذا كنت قد استخدمت JavaScript من قبل، فقد تلاحظ أننا نستورد الحزم بطريقة مختلفة. يجب علينا القيام بشيء مثل:
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. سيتم تخزينه محلياً في ذاكرة التخزين المؤقت (cache) الخاصة بك.
في السطر التالي، نقوم بما يلي:
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، يمكنك مراجعة هذا الدليل المبسط: Basic types by Typescript. ألقِ نظرة سريعة لمدة 2-3 دقائق ثم عد إلى هنا.
وفي النهاية، لدينا:
console.log('running on port ', port);
await app.listen({ port });
نقوم هنا ببساطة بطباعة رقم المنفذ في وحدة التحكم ونطلب من Deno الاستماع على المنفذ 8080. لا يقوم الخادم بالكثير في الوقت الحالي. دعنا نجعله يقوم بشيء أساسي مثل عرض رسالة JSON في متصفحك عند الانتقال إلى http://localhost:8080.
إضافة مسار أساسي (Basic 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. سترى شيئاً كهذا:

بصراحة، الجزء الأصعب قد انتهى. من الناحية المفاهيمية، نحن في 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 هو قيمة منطقية (boolean)، و hostname هو سلسلة نصية (string)، و port هو رقم. الآن عندما نبدأ تطبيقنا، سيطبع الرسالة في وحدة التحكم فقط بمجرد أن يبدأ التطبيق فعلياً بالاستماع على المنفذ.
يمكننا المضي قدماً خطوة واحدة وجعلها أكثر حيوية. دعنا نضيف وحدة (module) جديدة إلى أعلى الملف في server.ts:
import { green, yellow } from "https://deno.land/std@0.53.0/fmt/colors.ts";
ثم داخل دالة مستمع الأحداث (event listener) يمكننا استبدال:
console.log(`Listening on: ${port}`);
بـ:
console.log(`${yellow("Listening on:")} ${green(url)}`);
الآن عندما نقوم بـ:
$ deno run --allow-net server.ts
سيظهر هذا في وحدة التحكم لدينا:

رائع، الآن لدينا وحدة تحكم ملونة. إذا واجهتك أي مشكلة، يمكنك ببساطة الانتقال إلى الكود المصدري لهذا الدليل هنا: Chapter 1: Oak.
إنشاء مسارات ووحدات تحكم (Routes and Controllers)
لنقم بإنشاء مسارات واجهة API للمهام (Todo API) تالياً. أنشئ مجلداً جديداً في مجلدك الجذر يسمى routes وداخله أنشئ ملفاً يسمى todo.ts. في نفس الوقت، في مجلدك الجذر، أنشئ مجلداً جديداً يسمى controllers وداخله أنشئ ملفاً يسمى todo.ts.
ملف وحدة التحكم (Controller File)
لنبدأ بملف controllers/todo.ts:
export default {
getAllTodos: () => {},
createTodo: async () => {},
getTodoById: () => {},
updateTodoById: async () => {},
deleteTodoById: () => {},
};
نحن هنا ببساطة نقوم بتصدير كائن يحتوي على بعض الدوال المسماة التي هي فارغة (في الوقت الحالي).
ملف المسارات (Routes File)
بعد ذلك، انتقل إلى ملف 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 ثم إعداد نسخة جديدة من Router عن طريق 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 وقم بما يلي:
أضف هذا في الأعلى:
// routes
import todoRouter from "./routes/todo.ts";
أزل جزء الكود هذا:
const router = new Router();
router.get("/", ({ response }: { response: any }) => {
response.body = { message: "hello world" };
});
app.use(router.routes());
app.use(router.allowedMethods());
استبدله بـ:
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 });
إذا واجهتك أي مشكلة أثناء اتباع هذا، يمكنك ببساطة التوجه إلى الكود المصدري لهذا الدليل هنا: Chapter 1: Oak.
رائع، الآن لدينا مساراتنا بدون أي وظائف في الوقت الحالي. لذا، دعنا نضيف هذه الوظائف في وحدات التحكم الخاصة بنا. ولكن قبل أن نفعل ذلك، يجب علينا إنشاء ملفين آخرين (صغيرين).
تعريف الواجهات والبيانات الوهمية (Interfaces and Stubs)
في مجلدك الجذر، أنشئ مجلداً جديداً يسمى interfaces وداخله أنشئ ملفاً يسمى Todo.ts (تأكد من أن Todo مكتوب بحرف كبير، حيث لن يعطي أي خطأ في بناء الجملة هنا إذا لم تفعل ذلك – هذه مجرد اتفاقيات).
أيضاً في مجلدك الجذر، أنشئ مجلداً جديداً يسمى stubs وداخله أنشئ ملفاً يسمى todos.ts.
إنشاء واجهة (Interface)
لنقم بإنشاء واجهة في ملف 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)
الآن بعد أن أصبحت واجهتنا جاهزة، دعنا ننشئ بعض البيانات الوهمية (بما أننا لا نملك قاعدة بيانات فعلية لهذا الدليل). لنقم بإنشاء قائمة وهمية من المهام (todos) أولاً في ملف 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 بأن مصفوفة المهام (todos) الخاصة بنا هي من النوع Todo (وهو أمر رائع، فالمترجم الخاص بنا يعرف تلقائياً أن كل عنصر في مصفوفتنا يمكن أن يحتوي فقط على {id: string, todo: string & isCompleted: boolean} ولن يقبل أي مفتاح آخر). إذا أردت معرفة المزيد عن الواجهات (interfaces) في TypeScript، تحقق من هذه الوثائق المفصلة الرائعة حول الواجهات هنا.
رائع. إذا وصلت إلى هذا الحد، فربت على ظهر نفسك. عمل جيد يا رفاق.

تطوير وظائف وحدات التحكم (Controllers)
في ملف controllers/todo.ts الخاص بك:
export default {
getAllTodos: () => {},
createTodo: async () => {},
getTodoById: () => {},
updateTodoById: async () => {},
deleteTodoById: () => {},
};
1. جلب جميع المهام (GET /todos)
لنكتب وحدة التحكم الخاصة بـ 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: () => {},
};
قبل أن أبدأ في شرح هذا الجزء من الكود، دعني أوضح أن كل وحدة تحكم (controller) لها وسيط (argument) – دعنا نسميه context. لذا يمكننا تفكيك getAllTodos: (context) => {} إلى:
getAllTodos: ({ request, response, params }) => {}
وبما أننا نستخدم TypeScript، يجب علينا إضافة التحقق من النوع (type checking) لجميع هذه المتغيرات:
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 كائن الاستجابة (response object). يقوم Deno بذلك تلقائياً لنا. كل ما علينا فعله هو تعيين response.status وهو في هذه الحالة 200. المزيد عن حالات الاستجابة (response statuses) هنا.
الشيء الآخر الذي نحدده هو response.body وهو في هذه الحالة كائن:
{ success: true, data: todos }
سأقوم بتشغيل الخادم الخاص بي:
$ deno run --allow-net server.ts
مراجعة: تخبر الخاصية --allow-net Deno بأن هذا التطبيق يمنح المستخدم الإذن بالوصول إلى محتواه عبر المنفذ المفتوح. بمجرد تشغيل الخادم الخاص بك، يمكنك الوصول إلى واجهة API من نوع GET /todos. أنا أستخدم Postman وهو امتداد لـ Google Chrome ويمكن تنزيله هنا. يمكنك استخدام أي عميل REST تفضله. أنا أحب استخدام Postman لأنه سهل الاستخدام جداً. في Postman، افتح علامة تبويب جديدة. اضبط نوع الطلب على GET وفي شريط URL اكتب http://localhost:8080/todos. اضغط على Send وهذا ما ستراه:

رائع! تم إنجاز واجهة API واحدة، وبقيت 4 أخرى. إذا شعرت بالضياع في أي مكان، ما عليك سوى إلقاء نظرة خاطفة على الكود المصدري مباشرة هنا.
2. إضافة مهمة جديدة (POST /todos)
لننتقل إلى وحدة التحكم التالية:
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: () => {},
};
بما أننا سنضيف مهمة جديدة إلى قائمتنا، فقد قمت باستيراد وحدتين (modules) في ملف وحدة التحكم:
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 الذي أرسله المستخدم إلينا. ثم نستخدم دالة request.hasBody المضمنة في oak للتحقق مما إذا كان المستخدم قد أرسل أي محتوى على الإطلاق. إذا لم يكن كذلك، يمكننا تنفيذ الكود داخل كتلة if (!request.hasBody) {}. هنا، نضبط الحالة على 400 (400 تعني أن المستخدم قام بشيء لم يكن من المفترض أن يفعله) ويتم ضبط الجسم على {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,
};
نقوم بإنشاء مهمة جديدة عن طريق:
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 وهذا ما تراه:

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

رائع، طلب POST /todos مع محتوى الجسم { todo: "eat a lamma" } ناجح ويمكننا رؤية المحتوى المضاف إلى قائمة المهام الحالية لدينا. رائع، يمكننا أن نرى أن واجهة API الخاصة بنا تعمل كما هو متوقع. تم إنجاز واجهتي API، وبقيت ثلاث أخرى. نحن على وشك الانتهاء. معظم العمل الشاق قد تم. ☺️ 🎉 🎉 🎉
3. جلب مهمة بواسطة المعرف (GET /todos/:id)
لننتقل إلى واجهة 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;
}
في الجزء الأول، ننشئ ثابت todo جديد ونضبط نوعه إما على Todo أو undefined. لذا، سيكون todo إما كائناً له شكل واجهة Todo أو سيكون undefined – لا يمكن أن يكون أي شيء آخر. ثم نستخدم todos.find((t) => t.id === params.id) للبحث عن المهمة (todo) بالمعرف المقدم في params.id. إذا تطابق، نحصل على مهمة (Todo) بالشكل المحدد، وإلا نحصل على undefined. إذا كان todo غير معرف (undefined)، فهذا يعني أن كتلة if هذه ستعمل:
if (!todo) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
هنا ببساطة نضبط الحالة على 404 والتي تعني “غير موجود” (not found) جنباً إلى جنب مع استجابتنا الفاشلة القياسية أو { status, message }. رائع، أليس كذلك؟
بعد ذلك، نقوم ببساطة بما يلي:
// If todo is found
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. بما أننا نولد المعرفات عشوائياً، احصل أولاً على جميع المهام عن طريق طلب واجهة API لجلب جميع المهام. ثم من أي مهمة، احصل على أحد معرفاتها لاختبار واجهة API التي تم إنشاؤها حديثاً. في كل مرة تعيد فيها تشغيل تطبيق Deno هذا، سيتم إنشاء معرفات جديدة. لنبدأ:

حالة 404، لم يتم العثور على سجل.

تم توفير معرف معروف وعاد بالمهمة المرتبطة بهذا المعرف مع حالة 200. إذا كنت بحاجة إلى الرجوع إلى الكود المصدري الأصلي لهذا الدليل، فانتقل هنا. رائع، تم إنجاز 3 واجهات API، وبقيت 2 أخرى.
4. تحديث مهمة بواسطة المعرف (PUT /todos/:id)
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;
}
هذا شيء فعلناه تماماً بنفس الطريقة مع وحدة التحكم السابقة أيضاً، لذلك لن أخوض في الكثير من التفاصيل هنا. نصيحة احترافية هنا: يمكنك إذا أردت جعل هذا الجزء من الكود كتلة كود عامة ثم استخدامها في كلتا وحدتي التحكم.
بعد ذلك، نقوم بما يلي:
// 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,
};
الجزء من الكود الذي أريد التحدث عنه هنا هو التالي:
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. بما أننا نولد المعرفات عشوائياً، احصل أولاً على جميع المهام عن طريق طلب واجهة API لجلب جميع المهام. ثم من أي مهمة، احصل على أحد معرفاتها لاختبار واجهة API التي تم إنشاؤها حديثاً. في كل مرة تعيد فيها تشغيل تطبيق Deno هذا، سيتم إنشاء معرفات جديدة.

تم إرجاع حالة 404 ورسالة خطأ “لم يتم العثور على مهمة”.

تم توفير معرف معروف، وتم تحديث محتوى المهمة في الجسم. عادت المهمة المحدثة جنباً إلى جنب مع جميع المهام الأخرى. هذا مذهل – تم إنجاز أربع واجهات API وبقيت واحدة فقط.
5. حذف مهمة بواسطة المعرف (DELETE /todos/:id)
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 ونعيد الباقي. ثم نقوم بما يلي:
// remove the todo w.r.t id and return
// remaining todos
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. بما أننا نولد المعرفات عشوائياً، احصل أولاً على جميع المهام عن طريق طلب واجهة API لجلب جميع المهام. ثم من أي مهمة، احصل على أحد معرفاتها لاختبار واجهة API التي تم إنشاؤها حديثاً. في كل مرة تعيد فيها تشغيل تطبيق Deno هذا، سيتم إنشاء معرفات جديدة.

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

الآن لم يتبق لدينا سوى شيئين:
- إضافة وسيط (
middleware) للمسارات غير الموجودة (not found route) بحيث عندما يحاول المستخدم الوصول إلى مسار غير معروف، فإنه يعطي خطأ. - إضافة واجهة
APIمسجلة (logger API) تطبع في وحدة التحكم وقت الاستجابة الذي استغرقه إرجاع البيانات من نقطة نهاية واجهةAPIواحدة.
إنشاء وسيط للمسارات غير الموجودة (404 Not Found Middleware)
في مجلدك الجذر، أنشئ مجلداً جديداً يسمى 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 الخاص بك وأضف المحتوى التالي:
أضف هذا الاستيراد في مكان ما في الأعلى:
// not found
import notFound from './middlewares/notFound.ts';
ثم مباشرة أسفل app.use(todoRouter.allowedMethods()) أضف هذا السطر هكذا:
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.

لذا، لدينا الآن وسيط مسار (route middleware) وضعناه في نهاية مساراتنا في server.ts كـ app.use(notFound);. إذا لم يتطابق أي مسار مع هذا الوسيط، فسيتم تنفيذه وإرجاع رمز حالة 404 (والذي يعني “غير موجود”). ثم نرسل ببساطة رسالة استجابة كالعادة وهي {success, message}.
نصيحة احترافية: لقد قررنا أن {success, message} هو ما نرجعه في سيناريوهات الفشل وأن {success, data} هو ما نرجعه للمستخدم في سيناريوهات النجاح. لذا يمكننا حتى جعل هذه الكائنات/الأشكال واجهات وإضافتها إلى مشروعنا لضمان الاتساق والتحقق الآمن من النوع.
رائع، الآن انتهينا من أحد وسيطاتنا – دعنا نضيف الوسيط الآخر لتسجيل واجهات API الخاصة بنا في وحدة التحكم. تذكير: إذا واجهتك أي مشكلة، يمكنك استخدام الكود المصدري هنا.
تسجيل واجهات API في وحدة التحكم (API Logging Middleware)
في مجلد 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 الخاص بك، أضف هذا الكود:
استورد هذا في مكان ما في الأعلى:
// logger
import logger from './middlewares/logger.ts';
مباشرة فوق كود todoRouter الخاص بك، أضف هذه الوسيطات هكذا:
// 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);
دعنا الآن نناقش ما يحدث في كود وسيط التسجيل الخاص بنا ونناقش أسلوب تنفيذه باستخدام next():

الفرق الوحيد هنا وفي وحدات التحكم التي كانت لدينا من قبل هو استخدام دالة 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());
ترتيب التنفيذ هو كما يلي:
- وسيط
logger.logger - وسيط
logger.responseTime - وحدة تحكم
todoRouter(أياً كان المسار الذي يستدعيه المستخدم، لغرض الشرح، أفترض أن المستخدم استدعى واجهةAPIمن نوعGET /todosللحصول على جميع المهام).
لذا، سيتم تنفيذ وسيط logger.logger أولاً وهو هذا:
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()، فإنه يقفز بسرعة إلى الوسيط التالي وهو responseTime:

داخل 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.

بعد ذلك، نقوم ببساطة بتعيين الرؤوس في response الخاص بنا هكذا:
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.

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

يتم تسجيل واجهة API في وحدة التحكم لدينا. هذا كل شيء – لقد انتهينا. إذا كنت لا تزال تشعر بالضياع، ألقِ نظرة على الكود المصدري الكامل لهذا الدليل هنا: github.com/adeelibr/deno-playground/tree/master/chapter_1:oak.
آمل أن تكون قد وجدت هذه المقالة مفيدة وأنها تمكنت من مساعدتك في تعلم شيء جديد اليوم. إذا أعجبتك، يرجى مشاركتها على وسائل التواصل الاجتماعي. إذا كنت ترغب في مناقشة حولها، تواصل معي على Twitter.
الخلاصة التقنية
لقد قدم هذا الدليل رحلة متعمقة في بناء واجهة برمجة تطبيقات RESTful API باستخدام Deno وإطار عمل Oak. من خلال التركيز على المفاهيم الأساسية مثل إدارة المسارات (routing)، ومعالجة الطلبات والاستجابات (request/response handling)، واستخدام الواجهات (interfaces) في TypeScript لضمان سلامة الأنواع (type safety)، تمكنا من إنشاء تطبيق Todo API وظيفي بالكامل. كما سلطنا الضوء على أهمية الوسيطات (middlewares) في معالجة الأخطاء (مثل 404 Not Found) وتسجيل الأداء (performance logging)، مما يعزز من قوة ومرونة Deno كبيئة تشغيل حديثة. يبرز Deno بفضل أمانه المدمج (مثل --allow-net) ونظام الوحدات النمطية (module system) القائم على عناوين URL، مما يجعله خياراً ممتازاً لتطوير الواجهات الخلفية الحديثة.