6510 words
33 minutes
ابني HTTP Server من غير Express — إيه اللي تحت الغطاء (اليوم التالت)

1. ليه نبدأ من غير Express؟ وليه الأغلبية بتغلط في ده#

لو قولت لأي Junior: “ابني API”، أول حاجة هيعملها إنه يكتب npm install express ويبدأ يمشي بالـ app.get و app.post. وده منطقي — Express مريحة وبتسرع الإنتاجية. بس الكارثة إن المطور بيبقى بيستخدم أداة من غير ما يفهم إيه اللي بتعمله تحت. زي ما لو حد عايز يبقى مهندس عمارة وبيلعب LEGO بس.

// تفكير الـ Junior — يبدأ من Express فوراً:
const express = require('express');
const app = express();
app.get('/users', (req, res) => res.json({ users: [] }));
app.listen(3000);
// شغال؟ أيوه. فاهم إيه اللي بيحصل تحت؟ لأ.

ليه لازم تفهم الأساس الأول؟

  1. لأن Express مجرد Wrapper: كل حاجة Express بتعملها هي في الأصل سطور كود بتستخدم الـ http module الأصلي بتاع Node.js. الـ app.get('/users', handler) بتاعت Express تحتها مجرد if (req.method === 'GET' && req.url === '/users'). لما تفهم ده، Express هتبقى أداة واضحة مش سحر غامض.

  2. لأن مشاكل الـ Production مش بتظهر في الـ Framework: لما تواجه مشكلة في الـ Headers أو الـ Body مش بيتقرأ صح أو الـ Encoding باظ، لو مش فاهم إزاي الـ HTTP Protocol بيشتغل من الأساس، هتقعد تعيط على StackOverflow من غير ما تفهم السبب الحقيقي.

  3. لأن الانترفيوهات بتسألك على الأساس: في أي انترفيو Backend محترم، السؤال مش “اعمل API بـ Express”. السؤال: “اشرحلي إيه الفرق بين req في Node الأصلي و req في Express” أو “إزاي تتعامل مع الـ Body من غير body-parser؟”

الفكرة: Express مجرد طبقة تجميل (Abstraction Layer) فوق الـ http module. لما تفهم اللي تحت، كل فريم وورك (Express، Fastify، Hapi، Koa) هيبقى مفهوم ليك من أول نظرة — لأن كلهم بيعملوا نفس الحاجة بأسلوب مختلف.

تعال نرجع للتشبيه بتاعنا من اليوم التاني: الجرسون (الـ Event Loop) بياخد أوردرات ويوديها للمطبخ. النهاردة هنفهم حاجة مهمة: الأوردر نفسه (الـ HTTP Request) شكله إيه بالظبط؟ وإزاي الجرسون بيقرأه ويفهمه ويرد عليه؟


2. الـ http Module: قلب Node.js النابض#

الـ http Module ده إيه بالظبط؟#

الـ http module هو مكتبة مدمجة (Built-in) في Node.js. مش محتاج تعمل npm install — هي موجودة يوم ما Node يتسطب على جهازك. هي الطبقة اللي بتفهم بروتوكول HTTP وبتديك القدرة إنك تفتح سيرفر بيستمع على Port معين وبيستقبل الطلبات وبيرد عليها.

تعال نبني أول سيرفر في أقل من 10 سطور:

const http = require('http');

// الدالة دي هي الـ "Request Handler" — بتتنادى مع كل Request
const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end('مرحباً من سيرفر Node.js الخام!');
});

server.listen(3000, () => {
    console.log('السيرفر شغال على http://localhost:3000');
});

مفيش سطر هيعدي من غير تفسير#

  1. http.createServer(callback): الدالة دي بتعمل حاجة بسيطة وعبقرية في نفس الوقت: بتفتح TCP Socket (فاكر كوبري الـ TCP من اليوم الأول؟) وبتقول لنظام التشغيل: “أنا عايز أستقبل اتصالات على Port 3000”. كل ما أي Request HTTP يوصل، الـ callback function اللي إنت كاتبها بيتنادى. الـ Callback ده بياخد parameter اتنين: الـ req (الطلب اللي جاي من المتصفح) والـ res (الرد اللي هتبعته أنت).

  2. res.writeHead(200, headers): هنا إنت بتكتب بداية الرد — الـ Status Code (200 = كل حاجة تمام) والـ Headers (معلومات إضافية عن الرد، زي نوع المحتوى). خلي بالك: لازم تنادي writeHead قبل ما تبعت أي Body. لو بعت Body الأول، Node هيعمل headers تلقائية لكنك تفقد السيطرة.

  3. res.end(body): الدالة دي بتعمل حاجتين: بتبعت الـ Body (لو فيه) وبتقفل الاتصال. بدون res.end()، المتصفح هيفضل يلف في دائرة مستني الرد وفي الآخر هيطلع Timeout. ده من أكتر الأخطاء اللي الـ Juniors بيقعوا فيها — ينسوا الـ res.end() في حالة Error مثلاً، والسيرفر “يعلّق” على المستخدم.

  4. server.listen(port, callback): هنا السيرفر بيبدأ يشتغل فعلاً. قبل الـ listen، السيرفر مجرد Object في الميموري ومش بيعمل أي حاجة. الـ listen هي اللي بتفتح الـ Port وبتخلي نظام التشغيل يعرف إن في برنامج مستني اتصالات على الـ Port ده.

التشبيه: المكتب الأمامي (Reception Desk)#

تخيل إن http.createServer هو إنك فتحت مكتب استقبال في شركة. الـ listen(3000) هو إنك علقت لافتة على الباب رقم 3000 كتبت عليها “نستقبل الزبائن هنا”. كل زبون (Request) بيدخل من الباب ده، بيلاقي الموظف (الـ callback function) اللي بيسأله: “عايز إيه؟” (بيقرأ الـ req)، وبيرد عليه (بيكتب في الـ res).


3. تشريح الطلب والرد (Request & Response Objects)#

الـ Request Object (req) — “الورقة اللي الزبون بيقدمها”#

الـ req في الـ http module الأصلي هو Object من نوع http.IncomingMessage. ده مش الـ req الفخم بتاع Express اللي فيه req.body و req.params جاهزين. ده الـ Request الخام — المادة الأولية اللي Express نفسها بتبني عليها.

تعال نشوف إيه اللي جواه:

const server = http.createServer((req, res) => {
    console.log('=== طلب جديد وصل ===');
    console.log('الـ Method:', req.method);        // GET, POST, PUT, DELETE
    console.log('الـ URL:', req.url);               // /users?page=2
    console.log('الـ Headers:', req.headers);       // { 'content-type': '...', 'authorization': '...' }
    console.log('الـ HTTP Version:', req.httpVersion); // 1.1
    
    res.end('شوف الـ Console!');
});
الخاصيةالوصفمثال
req.methodنوع الطلب — الفعل اللي المتصفح عايزه"GET", "POST", "DELETE"
req.urlالمسار الكامل بما فيه الـ Query String"/users?page=2&limit=10"
req.headersObject فيه كل الـ Headers بحروف صغيرة{ "content-type": "application/json" }
req.httpVersionإصدار بروتوكول الـ HTTP"1.1"

ملاحظة مهمة: الـ req.url بيجيلك كله حتة واحدة: /users?page=2&limit=10.

Express بتفصلهملك في req.path و req.query، بس هنا لازم تفصلهم بإيدك. هنشوف ده بالتفصيل في قسم الـ Routing.

الـ Request كـ Stream — إزاي الـ Body بيوصلك وإزاي تتعامل معاه#

تمهيد: إيه الـ Stream وإيه الـ Chunk؟

الـ req مش مجرد Object فيه method و url و headers. من ناحية البيانات، هو Readable Stream — يعني مصدر بيانات بيوصلك على مراحل مش دفعة واحدة. ليه؟ لأن الطلب HTTP بييجي من العميل عبر الشبكة على شكل حزم (packets). كل ما حزمة توصل، Node يديّك جزء من الداتا اسمه Chunk. الـ Chunk = حتة من الـ Body (عادة عبارة عن Buffer — أي بيانات خام بايت بايت). فمفيش حاجة اسمها “الـ Body كله جاهز من أول ثانية”؛ الـ Body بيتكوّن مع وصول كل chunk لحد ما الاتصال يخلص و الـ Stream يقفل. عشان كده في الـ Node الخام مفيش req.body جاهز زي في Express. Express بيخفي عنك الموضوع لأنه بيجمع الـ Chunks دي ورا الكواليس (عبر middleware زي body-parser) ويديك النتيجة في req.body. هنا انت اللي تجمع.

إزاي تجمع الـ Body بنفسك؟

الفكرة: متغير واحد (مثلاً body) تبدأه فاضي، وكل ما حدث 'data' يحصل (يعني chunk جديد وصل)، تضيف الـ chunk للمتغير. لما حدث 'end' يحصل (يعني الـ Stream خلص ومفيش حاجة جاية بعد كده)، يبقى عندك الـ Body الكامل وتقدر تستخدمه (تقراه، تعمل parse لـ JSON، إلخ).

// في Node الخام مفيش req.body — لازم تجمع الـ Body من الـ Stream
const server = http.createServer((req, res) => {
    // متغير نجمّع فيه كل الـ Chunks اللي هتوصل
    let body = '';

    // حدث 'data': بيحصل كل ما حتة جديدة من الداتا توصل من الشبكة
    req.on('data', (chunk) => {
        // الـ chunk بتيجي كـ Buffer (بايتات خام). لو الـ Body نص (زي JSON)، حوّله لـ String عشان تقدر تضيفه لـ body
        body += chunk.toString();
    });

    // حدث 'end': بيحصل لما الـ Stream يخلص — مفيش chunks جديدة جاية
    req.on('end', () => {
        console.log('الـ Body الكامل:', body);
        // هنا تقدر تعمل JSON.parse(body) لو الطلب أرسل JSON
        res.end('استلمنا الداتا!');
    });
});

شرح سريع سطر سطر:

السطر / الجزءإيه اللي بيحصل
let body = ''مكان نجمّع فيه نص الـ Body. نبدأه فاضي.
req.on('data', (chunk) => { ... })كل ما chunk جديد يوصل (داتا من الجسم بتاع الطلب)، الـ Callback دي بتتنفذ. الـ chunk متغير فيه الحتة دي من الداتا (نوعه Buffer).
chunk.toString()الـ Buffer عبارة عن بايتات. لو الـ Body نص (مثلاً JSON)، بنحول الـ chunk لـ String عشان نقدر نضمّه لـ body بالـ +=.
req.on('end', () => { ... })لما الـ Stream يخلص (كل الـ Body اتبعت)، الـ Callback دي بتتنفذ مرة واحدة. في اللحظة دي body فيه الـ Body كامل.

متى يكون في Body أصلاً؟
في طلبات GET غالباً مفيش Body (الداتا في الـ URL أو الـ Query). الـ Body بيبقى في POST و PUT و PATCH (مثلاً فورم، أو JSON من العميل). ففي كود حقيقي ممكن تسمع لـ 'data' و 'end' بس لو req.method === 'POST' أو لو في Content-Length أكبر من صفر، عشان متتعاملش مع طلبات مفيش فيها body.

ليه مش بيوصل دفعة واحدة؟ ليه الـ Stream أصلاً؟

لو الطلب كان بيوصل كله مرة واحدة، يبقى السيرفر لازم يخصص مكان في الذاكرة لكل الطلب من أول ما يبدأ لحد ما يخلص. تخيل يوزر رافع ملف 500MB أو عميل بعت Body حجمه 500MB. لو Node استنى الخمسمية ميجا كلها توصل وحطها في الـ RAM دفعة واحدة، ممكن الذاكرة تنفجر أو السيرفر يبطئ جداً. بالـ Streams، Node (والشبكة) بيشتغلوا حتة حتة: مثلاً كل مرة توصل 64KB، السيرفر يقراها ويعالجها (أو يكتبها على الديسك) ويمسحها من الذاكرة، وبعدين يستنى الـ 64KB اللي بعدها. كده حجم الذاكرة المستخدمة ثابت تقريباً (بحجم الـ chunk) مش بحجم الملف كله. عشان كده حتى لو الملف 10GB، الميموري هتفضل تحت السيطرة — المهم إنك ما تخزنش الـ Body كله في متغير واحد لو الحجم كبير جداً؛ لو متوقع Body ضخم، استقبله على شكل Stream واكتبه على ملف أو عالجه chunk chunk بدل ما تجمعه كله في body.

من الآخر: الـ req= Readable Stream في Node

الـ Body بيوصلك على شكل Chunks (كل chunk = Buffer). بتجمعه بـ req.on('data', ...) وتستغل النتيجة النهائية في req.on('end', ...). مفيش req.body جاهز — ده سلوك Express اللي بيجمعه عنك. الـ Streams موجودة عشان نمنع انفجار الذاكرة لما الداتا كبيرة.

الـ Response Object (res) — إزاي تبعت الرد للعميل من الصفر#

تمهيد: إيه الـ res وإيه شكل الرد HTTP؟

الـ res هو الـ Object اللي تمثّل الرد اللي السيرفر هيرسله للعميل. نوعه http.ServerResponse وهو Writable Stream — يعني إنت بتكتب فيه الداتا اللي عايز تبعتها، و Node بيبعت الداتا دي على الشبكة للعميل. الفكرة: ما تبعتش الرد كله مرة واحدة بالضرورة؛ تقدر تكتب حتة بـ res.write() ثم حتة تانية ثم … وتقفل الرد وتقول “خلصت” بـ res.end(). أو تقدر تبعت كل حاجة مرة واحدة بـ res.end(الكل). المهم إن الرد HTTP له تلات أجزاء لازم تعرفهم:

  1. Status Code (رمز الحالة): رقم من 3 أرقام (مثلاً 200، 404، 500) بيقول للعميل إيه حالة الطلب — نجح، ملقيناش الصفحة، غلط في السيرفر، إلخ.
  2. Headers (الترويسات): معلومات عن الرد (مش المحتوى نفسه) — نوع المحتوى (نص؟ JSON؟ صورة؟)، الطول، الكوكيز، CORS، إلخ. لازم الـ Headers تتبعت قبل أي جزء من الـ Body.
  3. Body (الجسم): المحتوى الفعلي اللي العميل بيقراه — مثلاً نص الـ HTML أو الـ JSON أو صورة. أي حاجة بعد الـ Headers هي الـ Body.

في Node الخام، إنت اللي تحدد الـ Status والـ Headers والـ Body وتكتبهم على الـ res بالترتيب الصحيح. لو نسيت res.end()، الـ Stream مابقاش مقفول والعميل هيستنى للأبد (اتصال معلق).

مثال عملي: Status + Headers + Body

const server = http.createServer((req, res) => {
    // 1) Status Code — رقم الحالة (200 = نجاح)
    res.statusCode = 200;

    // 2) Headers — معلومات عن الرد (لازم قبل أي كتابة Body)
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.setHeader('X-Powered-By', 'Our Raw Node Server');  // Header مخصص

    // 3) Body — المحتوى الفعلي، وبعدها نقفل الرد بـ res.end()
    const data = JSON.stringify({ message: 'أهلاً بيك!', status: 'success' });
    res.end(data);
});

شرح سطر سطر:

الجزءإيه اللي بيحصل
res.statusCode = 200بتحدد رمز الحالة. 200 = OK (الطلب نجح). لو ماحددتهوش يبقى 200 افتراضي. أرقام تانية: 404 (Not Found)، 500 (Server Error)، 201 (Created)، 401 (Unauthorized)، إلخ.
res.setHeader(name, value)بتضيف Header واحد للرد. الـ Headers بتتبعت قبل الـ Body. مثلاً Content-Type بيقول للعميل “الـ Body ده JSON بنص UTF-8”.
res.end(data)بتكتب آخر حتة من الـ Body (أو الـ Body كله لو مكتوبتش قبل كده بـ res.write) وبتقفل الرد — بعدها مينفعش تكتب تاني على الـ res. لازم تنادي res.end() في كل طلب عشان العميل يعرف إن الرد خلص.

لو عايز تبعت الرد حتة حتة (Stream): تقدر تنادي res.write(chunk) أكتر من مرة (كل مرة بتبعت حتة)، وبعدين res.end() عشان تقفل. لو الرد صغير، res.end(data) وحدها كفاية — بتكتب الـ Body وتقفل.

الفرق بين res.setHeader() و res.writeHead():

الاتنين بيضبطوا الـ Status Code والـ Headers. الفرق في متى الـ Headers بتتبعت للعميل وفي أسلوب الاستخدام.

الطريقةالاستخدامالسلوك
res.setHeader(name, value)بتضبط Header واحد في أي وقت قبل ما يبدأ إرسال الـ Body. تقدر تناديها أكثر من مرة في أماكن مختلفة (مثلاً middleware يضيف CORS، و handler يضيف Content-Type).الـ Headers مبتتبعتش فوراً — Node بيجمعها وبيبعتهم مع أول سطر من الـ Body (أول res.write() أو res.end()). مرنة ومناسبة لما الـ Headers بتتحدد في أكثر من مكان.
res.writeHead(statusCode, headers)بتحدد الـ Status Code وكل الـ Headers مرة واحدة (الـ headers كـ Object).الـ Headers (والـ Status) بتتبعت فوراً على الشبكة. بعدها أي res.write() أو res.end() بيكون Body. لازم تناديها قبل أي كتابة Body. أسرع لما كل حاجة محددة من أول وهلة؛ لو ناديتها بعد res.write() أو بعد بدء الإرسال هتطلع خطأ.

مثال بـ writeHead:

res.writeHead(200, {
    'Content-Type': 'application/json; charset=utf-8',
    'X-Powered-By': 'Our Raw Node Server'
});
res.end(JSON.stringify({ message: 'أهلاً بيك!' }));

قاعدة عملية: لو الرد في مكان واحد وبسيط (كل الـ Status والـ Headers معروفة من أول الكود)، استخدم res.writeHead(). لو عندك middleware أو كود متفرع بيضيف Headers في أماكن مختلفة (CORS هنا، Auth هناك)، استخدم res.setHeader() وخلّي Node يبعت كل الـ Headers مع أول res.write() أو res.end().

من الآخر: الـ res = Writable Stream تمثّل رد HTTP. بتحدد Status Code و Headers (قبل الـ Body)، بعدين بتكتب الـ Body بـ res.write() و/أو res.end(). لازم تنادي res.end() في كل طلب عشان تقفل الرد والعميل ميبقاش مستني. setHeader مرن وبيتجمع مع أول إرسال؛ writeHead يضبط كل حاجة مرة واحدة ويبعت الـ Headers فوراً.


4. فن الـ Routing اليدوي: إزاي توزع الطلبات بإيدك#

المشكلة: كل الطلبات بتوصل لنفس الدالة!#

في Express كل Route ليه Handler منفصل. بس في الـ http module الأصلي، كل الطلبات — سواء GET أو POST، سواء /users أو /products أو /asdkjhaskd — كلها بتوصل لنفس الـ callback. إنت اللي مسؤول تفرز الطلبات وتوجهها.

تخيل إنك الريسبشنست في فندق كبير. كل الناس بتيجيلك — اللي عايز يحجز غرفة، اللي عايز يسأل عن المطعم، اللي تاه ودخل غلط. إنت اللي بتسألهم “عايز إيه؟” وبتوجههم.

الحل: الـ Router اليدوي#

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
    // أول حاجة: فصل الـ Path عن الـ Query String
    const parsedUrl = url.parse(req.url, true);
    const path = parsedUrl.pathname;      // "/users"
    const query = parsedUrl.query;        // { page: "2", limit: "10" }
    const method = req.method;            // "GET"

    // ضبط الـ Headers المشتركة
    res.setHeader('Content-Type', 'application/json; charset=utf-8');

    // === التوجيه اليدوي ===
    if (path === '/' && method === 'GET') {
        res.statusCode = 200;
        res.end(JSON.stringify({ message: 'مرحباً بيك في الـ API!' }));
    }
    else if (path === '/users' && method === 'GET') {
        res.statusCode = 200;
        res.end(JSON.stringify({ users: ['أحمد', 'سارة', 'محمد'], page: query.page || 1 }));
    }
    else if (path === '/health' && method === 'GET') {
        res.statusCode = 200;
        res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
    }
    else {
        // 404 — الطلب مطابقش أي Route
        res.statusCode = 404;
        res.end(JSON.stringify({ error: 'الصفحة دي مش موجودة!' }));
    }
});

server.listen(3000, () => console.log('السيرفر شغال على http://localhost:3000'));

إيه اللي بيحصل في الكود ده خطوة بخطوة؟#

  1. url.parse(req.url, true): الـ req.url بتيجيلك كنص خام زي /users?page=2&limit=10. الدالة url.parse بتفصله لأجزاء: الـ pathname (المسار بدون الـ query) والـ query (الباراميترز كـ Object). الـ true في الـ argument التاني معناها “حول الـ query string لـ Object بدل ما يبقى نص”.

  2. سلسلة الـ if/else: هنا إنت بتقارن الـ path والـ method عشان توجه الطلب. دي نفس الفكرة اللي Express بتعملها تحت الغطاء — بس Express بتعملها بشكل أنظف مع Pattern Matching وMiddleware Chain.

  3. الـ 404 في الآخر: أي طلب ممسكش بأي if بيقع في الـ else — وده لازم يكون دايماً موجود. بدونه، السيرفر هيعلّق على الطلب (مفيش res.end()) والمتصفح يفضل يلف.

المشكلة: الكود بقى قبيح!#

لو عندك 50 Route، الـ if/else هيبقى 200 سطر من الفوضى. تعال نعمل Router بسيط ونظيف:

const http = require('http');
const url = require('url');

// === هيكل الـ Router ===
const routes = {};

function addRoute(method, path, handler) {
    const key = `${method.toUpperCase()}:${path}`;
    routes[key] = handler;
}

function handleRequest(req, res) {
    const parsedUrl = url.parse(req.url, true);
    const path = parsedUrl.pathname;
    const method = req.method;
    const key = `${method}:${path}`;

    // مرّر الـ query كـ property إضافية على الـ req
    req.query = parsedUrl.query;

    res.setHeader('Content-Type', 'application/json; charset=utf-8');

    // دور على الـ Handler المطابق
    const handler = routes[key];
    if (handler) {
        handler(req, res);
    } else {
        res.statusCode = 404;
        res.end(JSON.stringify({ error: 'Route not found' }));
    }
}

// === تسجيل الـ Routes (شبه Express بالظبط!) ===
addRoute('GET', '/', (req, res) => {
    res.statusCode = 200;
    res.end(JSON.stringify({ message: 'الصفحة الرئيسية' }));
});

addRoute('GET', '/users', (req, res) => {
    res.statusCode = 200;
    res.end(JSON.stringify({ users: ['أحمد', 'سارة'], page: req.query.page || 1 }));
});

addRoute('GET', '/health', (req, res) => {
    res.statusCode = 200;
    res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
});

// === تشغيل السيرفر ===
const server = http.createServer(handleRequest);
server.listen(3000, () => console.log('السيرفر شغال!'));

إيه اللي بيحصل في الكود ده خطوة بخطوة؟#

تعال نفكك الـ Router من أول سطر لآخر سطر عشان تبقى فاهم إيه اللي بيحصل لما طلب يوصّل.


1) الـ “جدول” اللي بنحط فيه الـ Routes: const routes = {}

routes هو Object عادي (جدول فارغ في البداية). الفكرة: كل Route (زوج Method + Path) يبقى له مفتاح (key) و دالة (handler). المفتاح شكل ثابت: "METHOD:path" — مثلاً "GET:/" أو "GET:/users" أو "POST:/login". لما طلب يوصّل، نجمّع منه الـ Method والـ Path ونعمل منهم نفس الـ key، وندور في الجدول: لو لقينا handler نناديها، لو لأ نرد 404.


2) تسجيل Route: addRoute(method, path, handler)

الدالة دي بتضيف Route للجدول. بتاخد تلات حاجات:

  • method: نوع الطلب (GET، POST، …).
  • path: المسار (مثلاً '/' أو '/users').
  • handler: الدالة اللي هتتنفذ لما الطلب يطابق الـ Method والـ Path.

جوه الدالة: بنعمل key من الـ method والـ path (مثلاً "GET:/users") وبنحط الـ handler في routes[key]. بعد كده لو حد بعت طلب GET /users، الـ key هتكون "GET:/users" وهنلاقي الـ handler في الجدول.


3) معالجة أي طلب: handleRequest(req, res)

دي الدالة اللي Node بيناديها كل ما طلب يوصّل. كل اللي بتعمله: تفكّ الطلب، تعرف مين الـ Handler المناسب، وتناديها (أو ترد 404).

الخطوة في الكودإيه اللي بيحصل
url.parse(req.url, true)الـ req.url بيكون نص زي "/users?page=2&limit=10". الـ parse بتفصله: pathname = "/users" (المسار بدون الـ query)، query = { page: '2', limit: '10' } (الباراميترز كـ Object). الـ true معناها “حوّل الـ query string لـ Object”.
path و method و keyبناخد المسار من parsedUrl.pathname والـ Method من req.method، وبنجمّعهم في key بنفس الشكل اللي سجّلنا بيه الـ Routes: "GET:/users".
req.query = parsedUrl.queryبنحط الـ query على الـ req عشان أي handler يقدر يستخدمها مباشرة (مثلاً req.query.page).
res.setHeader('Content-Type', ...)بنضبط نوع الرد مرة واحدة لكل الطلبات (JSON).
const handler = routes[key]بندور في الجدول: في عندنا handler مسجّل للـ key دي؟
if (handler) { handler(req, res); }لو لقينا، بننادي الـ handler وبنمرّر لها req و res. هي اللي هتكتب الرد (مثلاً res.end(...)).
else { res.statusCode = 404; res.end(...); }لو ملقيناش Route مطابق، نرد 404 ونبعت رسالة إن الـ Route مش موجودة.

مهم: كل handler مسؤول إنه ينادي res.end() (أو يكتب ويفضل بـ res.end()). الـ handleRequest مش بتنادي res.end() للـ handlers — هي بس بتنادي الـ handler والـ handler هو اللي يرد على العميل.


4) تسجيل الـ Routes الثلاثة: addRoute('GET', '/', ...) و إلخ

قبل ما السيرفر يبدأ، بنملّي الجدول:

  • addRoute('GET', '/', ...) → أي طلب GET على المسار / هيشغّل الدالة اللي جوه (ترد “الصفحة الرئيسية”).
  • addRoute('GET', '/users', ...) → أي طلب GET على /users هيشغّل الدالة اللي جوه (ترد لستة يوزرز + تستخدم req.query.page).
  • addRoute('GET', '/health', ...) → أي طلب GET على /health هيشغّل الدالة اللي جوه (ترد حالة السيرفر).

بعد التسجيل، الـ routes بيكون فيه تلات مفاتيح: "GET:/" و "GET:/users" و "GET:/health".


5) تشغيل السيرفر: http.createServer(handleRequest) و listen(3000)

بنقول لـ Node: كل ما طلب يوصّل (أي Method وأي URL)، نفّذ handleRequest(req, res). فكل الطلبات بتمر من نفس الدالة، والدالة دي بتفكّ الـ URL والـ Method وتدور على الـ Handler المناسب من الجدول.


مثال: طلب GET /users?page=2 من المتصفح

  1. العميل يبعت طلب HTTP: السطر الأول بيكون زي GET /users?page=2 HTTP/1.1.
  2. Node ينادي handleRequest(req, res). الـ req.url = "/users?page=2".
  3. url.parse(req.url, true)pathname = "/users"، query = { page: '2' }.
  4. path = "/users"، method = "GET"key = "GET:/users".
  5. req.query يتبقى { page: '2' } عشان الـ handler يستخدمه.
  6. routes["GET:/users"] → موجود (الدالة اللي فيها users: ['أحمد', 'سارة']).
  7. بننادي الدالة دي: handler(req, res). هي تنفذ res.end(JSON.stringify({ users: [...], page: 1 })) (هنا req.query.page = '2' فتقدر تستخدمه في الرد الفعلي).
  8. الرد يتبعت للعميل والاتصال يقفل.

لو الطلب كان GET /unknown، الـ key هتكون "GET:/unknown"، ومفيش handler مسجّل → نروح للـ else ونرد 404.

تلاحظ الشبه؟ addRoute('GET', '/users', handler) — ده شبه app.get('/users', handler) في Express! الفرق إن Express بتضيف فوقه Middleware Pipeline و Pattern Matching (زي /users/:id) و Error Handling. بس الجوهر واحد: خد الـ Method والـ Path، دور على الـ Handler، نفذه.


5. قراية الـ Body: إزاي تستقبل بيانات من المستخدم#

المشكلة الكبيرة اللي مبيفهمهاش الـ Juniors#

في Express، تكتب app.use(express.json()) والدنيا تمشي — الـ req.body يطلعلك Object جاهز. بس في الواقع، الموضوع أعقد من كده بكتير. الـ Body في HTTP بيوصل كـ Raw Bytes في Stream، وإنت مسؤول عن:

  1. تجميع الـ Chunks (الأجزاء).
  2. تحويلها من Buffer لنص (String).
  3. التأكد إن النص ده JSON صالح (Parsing).
  4. التعامل مع الأخطاء (الـ JSON المكسور أو الـ Body الفاضي).

تعال نبني Helper Function تعمل ده كله:

/**
 * تقرأ الـ Body من الـ Request Stream وتحوله لـ Object
 * بترجع Promise عشان نستخدمها مع async/await
 */
function parseBody(req) {
    return new Promise((resolve, reject) => {
        let body = '';
        
        // حماية 1: لو مفيش Body أصلاً (GET أو DELETE عادي)
        if (req.method === 'GET' || req.method === 'DELETE') {
            return resolve(null);
        }

        // حماية 2: حد أقصى لحجم الـ Body (1MB) — عشان هاكر ميبعتلكش 10GB ويوقعك
        const MAX_SIZE = 1 * 1024 * 1024; // 1MB
        let totalSize = 0;

        req.on('data', (chunk) => {
            totalSize += chunk.length;
            if (totalSize > MAX_SIZE) {
                req.destroy(); // اقتل الاتصال فوراً!
                reject(new Error('الـ Body أكبر من اللي مسموح! (Max: 1MB)'));
                return;
            }
            body += chunk.toString();
        });

        req.on('end', () => {
            if (!body) return resolve(null); // Body فاضي

            try {
                const parsed = JSON.parse(body);
                resolve(parsed);
            } catch (err) {
                reject(new Error('الـ JSON مكسور! اتأكد من الصيغة.'));
            }
        });

        req.on('error', (err) => {
            reject(err);
        });
    });
}

إيه اللي بيحصل في الكود ده خطوة بخطوة؟#

تعال نفكك دالة parseBody من أول سطر لآخر سطر عشان تبقى فاهم إيه اللي بيحصل لما تستدعيها في الـ handler.


1) الهدف من الدالة وإيه اللي بترجعه

الدالة بتاخد الـ Request (req) وبتقرا الـ Body بتاعه من الـ Stream (زي ما شرحنا قبل كده: الـ Body بيوصل حتة حتة عبر حدث 'data' وبيخلص في حدث 'end'). بعد ما تجمع كل الـ Body كنص، بتحاول تحوّله لـ Object باستخدام JSON.parse. النتيجة: إما Object (لو الـ Body كان JSON صالح)، أو null (لو مفيش Body أو فاضي)، أو الـ Promise بترجع خطأ (reject) لو الـ Body أكبر من الحد أو الـ JSON مكسور أو الـ Stream فيه مشكلة. عشان الـ Body بيوصل بعد شوية (مش فوراً)، الدالة بترجع Promise عشان نستنى النتيجة بـ await parseBody(req).


2) return new Promise((resolve, reject) => { ... })

كل الشغل اللي جوه الدالة بيحصل جوه Promise. الـ resolve معناها “خلصت بنجاح ودي النتيجة” (هتناديها بـ resolve(الـ Object) أو resolve(null)). الـ reject معناها “حصل خطأ” (هتناديها بـ reject(Error)). لما نكتب const body = await parseBody(req) في الـ handler، الـ await بيستنى لحد ما الـ Promise تخلص — إما بـ resolve (ويبقى body = النتيجة) أو بـ reject (ويطلع Exception ونمسكها في try/catch).


3) let body = '' — مكان تجميع الـ Body

متغير نجمّع فيه كل الـ Chunks اللي هتوصل. كل ما حدث 'data' يحصل، هنضيف الـ chunk للمتغير ده. في النهاية body هيكون نص الـ Body الكامل (String).


4) حماية 1: لو الطلب GET أو DELETE

في طلبات GET و DELETE غالباً مفيش Body (الداتا في الـ URL أو الـ Query). فبدل ما نستنى أحداث الـ Stream من غير فايدة، بنرجع فوراً: return resolve(null). كده الـ Promise بتخلص مباشرة والـ handler يلاقي body = null.


5) حماية 2: حد أقصى لحجم الـ Body (1MB)

  • MAX_SIZE = 1 * 1024 * 1024 → 1 ميجابايت (بالبايت). لو الـ Body أكبر من كده، بنرفض الطلب.
  • let totalSize = 0 → بنعدّ حجم كل الـ Chunks اللي وصلت. كل ما chunk توصل، بنزود totalSize ونقارنها بـ MAX_SIZE.

ليه؟ عشان حد ما يبعتش Request Body حجمه 10GB ويخلي السيرفر يجمّع كل الـ Chunks في الـ RAM — ده يوقع السيرفر (Out of Memory). وضع حد أقصى اسمه Large Payload Attack protection.


6) حدث 'data': كل ما chunk توصل

السطرإيه اللي بيحصل
req.on('data', (chunk) => { ... })كل ما حزمة جديدة من الـ Body توصل من الشبكة، الـ Callback دي بتتنفذ. الـ chunk متغير فيه الحتة دي (نوعه Buffer).
totalSize += chunk.lengthبنزود الحجم الإجمالي اللي وصل لحد دلوقتي.
if (totalSize > MAX_SIZE)لو تجاوزنا 1MB، بنوقف: req.destroy() (بنقطع الاتصال عشان العميل ميكملش يبعت بيانات)، وبننادي reject(new Error(...)) عشان الـ Promise تفشل والـ handler يمسك الخطأ. بعدها return عشان م نضيفش الـ chunk للـ body.
body += chunk.toString()لو مفيش تجاوز للحد، بنحول الـ chunk لـ String ونضيفه لـ body.

7) حدث 'end': الـ Stream خلص

لما الـ Body كله يوصّل والـ Stream يقفل، حدث 'end' بيحصل مرة واحدة. هنا بنقرر إيه اللي نعمله بالـ body:

السطرإيه اللي بيحصل
if (!body) return resolve(null)لو الـ Body فاضي (نص فاضي أو مفيش حاجة اتبعت)، بنرجع null بدون ما نحاول نعمل JSON.parse.
try { const parsed = JSON.parse(body); resolve(parsed); }بنحاول نحول النص لـ Object. لو الـ JSON صحيح، بننادي resolve(parsed) عشان الـ Promise تخلص والـ handler يلاقي الـ Object.
catch (err) { reject(new Error('الـ JSON مكسور! ...')); }لو النص مش JSON صالح (مكسور أو نص عادي)، JSON.parse بيرمي Exception. بنمسكها وننادي reject عشان الـ Promise تفشل والـ handler يمسك الخطأ في try/catch.

8) حدث 'error': لو الـ Stream فيه مشكلة

لو حصل خطأ على الـ Request Stream (مثلاً الاتصال انقطع)، حدث 'error' بيحصل. بننادي reject(err) عشان الـ Promise ما تفضلش معلقة — الـ handler يمسك الخطأ في try/catch ويتعامل معاه.


9) ترتيب التنفيذ في الواقع

  • لو الطلب GET أو DELETE → الدالة ترجع فوراً resolve(null) ومفيش أحداث Stream.
  • لو الطلب POST (أو غيره وفيه Body): أولاً بيحصل أحداث 'data' (واحدة أو أكتر) — بنجمّع الـ Chunks ونتحقق من الـ MAX_SIZE. لو اتجاوزنا، destroy + reject ومفيش 'end'. لو مفيش تجاوز، لما الـ Stream يخلص بيحصل 'end' — بنعمل JSON.parse و resolve(parsed) أو resolve(null) أو reject لو الـ JSON مكسور. في أي وقت لو حصل 'error' على الـ Stream، بنعمل reject(err).

من الآخر: الدالة بتقرا الـ Body من الـ Stream (تجميع Chunks)، تتحقق من الحجم (حد 1MB)، وتحول النص لـ Object بـ JSON.parse. بترجع Promise: resolve(Object) أو resolve(null) أو reject(Error). عشان كده في الـ handler بتكتب const body = await parseBody(req) وتتعامل مع النتيجة أو تمسك الخطأ.


ليه كل سطر مهم#

  • الـ MAX_SIZE Check: بدون الحد ده، هاكر يقدر يبعت Request بـ Body حجمه 10GB. الـ Node هيفضل يجمع الـ Chunks في الـ RAM لحد ما السيرفر يقع بـ Out of Memory. ده هجوم اسمه Slowloris / Large Payload Attack. في Express، الـ express.json({ limit: '1mb' }) بيعمل نفس الحاجة بسطر واحد — بس تحتها هي نفس الفكرة.

  • الـ JSON.parse في try/catch: لو المستخدم بعت Body مش JSON صالح (مثلاً نص عادي أو XML)، الـ JSON.parse هيرمي Error. بدون الـ try/catch، الخطأ ده هيسافر لـ uncaughtException ويوقع السيرفر كله!

  • الـ req.destroy(): لما الـ Body يتجاوز الحد، مش بس بنرفض — بنقتل الاتصال بالكامل عشان ميكملش يبعتلنا بايتات وياخدلنا Bandwidth.

استخدام الـ parseBody في الـ Server#

addRoute('POST', '/users', async (req, res) => {
    try {
        const body = await parseBody(req);
        
        // لو مفيش body أو مفيش اسم
        if (!body || !body.name) {
            res.statusCode = 400;
            return res.end(JSON.stringify({ error: 'الاسم مطلوب!' }));
        }

        // في الواقع هنا هنحفظ في الداتابيز — حالياً هنرد بس
        const newUser = { id: Date.now(), name: body.name, email: body.email || null };
        
        res.statusCode = 201; // 201 = Created — العنصر اتعمل بنجاح
        res.end(JSON.stringify({ message: 'تم إنشاء المستخدم', user: newUser }));
    } catch (err) {
        res.statusCode = 400;
        res.end(JSON.stringify({ error: err.message }));
    }
});

من الآخر: Express مش بتعمل سحر. هي بتستخدم نفس الفكرة (Events على الـ Stream: data و end) بس مغلفاها في Middleware اسمه body-parser (أو express.json) عشان تريحك. لما تفهم الأساس، لو body-parser باظ أو عايز تعمل Custom Parser (لـ XML أو Protocol Buffers مثلاً)، هتعرف بالظبط إزاي.


6. الـ Status Codes: اللغة السرية بين السيرفر والمتصفح#

ليه الـ Status Code مهم؟#

الـ Status Code مش مجرد رقم. هو لغة موحدة الإنترنت كله متفق عليها بتقول للمتصفح (أو لأي Client) “إيه اللي حصل بالظبط”. لو رقم الرد غلط، حاجات كتير هتبوظ: الـ Frontend مش هيفهم إن في خطأ، المتصفح مش هيعمل Caching صح، ومحركات البحث مش هتفهرس صفحاتك صح.

الجدول الذهبي — أهم الأكواد اللي لازم تحفظها#

الكودالاسمالمعنىاستخدمه لما…
200OKكل حاجة تمامالطلب نجح ورجعت بيانات
201Createdتم الإنشاءأنشأت عنصر جديد (POST ناجح)
204No Contentتمام بس مفيش بيانات للردحذفت عنصر بنجاح (مفيش حاجة ترجعها)
400Bad Requestالطلب غلطالمستخدم بعت داتا ناقصة أو مكسورة
401Unauthorizedمين إنت؟المستخدم مش مسجل دخول (مفيش Token)
403Forbiddenأعرفك بس ممنوع!المستخدم مسجل دخول بس مش Admin
404Not Foundمش موجودالـ Route أو العنصر مش موجود
409Conflictتضارب!المستخدم بيحاول يسجل بإيميل موجود
422Unprocessable Entityالصيغة صح بس المحتوى غلطالـ JSON صح بس الإيميل مش Valid
429Too Many Requestsبطل تبعت كتير!الـ Rate Limiter مسك المستخدم
500Internal Server Errorالسيرفر وقعخطأ في الكود بتاعك (Bug)
502Bad Gatewayالبروكسي مش لاقي السيرفرالـ Nginx أو Load Balancer مش لاقي الـ Application Server
503Service Unavailableالسيرفر مشغولالسيرفر تحت ضغط أو في Maintenance

الفرق الدقيق اللي بيقع فيه الكل: 401 vs 403#

// 401 — المستخدم مبعتش Token أصلاً (مش معرّف نفسه):
if (!req.headers['authorization']) {
    res.statusCode = 401; // "مين أنت؟ عرّف نفسك الأول!"
    return res.end(JSON.stringify({ error: 'Authentication required' }));
}

// 403 — المستخدم معرف نفسه (Token صالح) بس مش مسموح ليه:
if (user.role !== 'admin') {
    res.statusCode = 403; // "أنا أعرفك يا أحمد، بس أنت مش Admin"
    return res.end(JSON.stringify({ error: 'Admin access only' }));
}

التشبيه: الـ 401 زي ما تروح مبنى حكومي من غير بطاقة — الحارس هيقولك “هات الهوية الأول”. الـ 403 زي ما تروح بالبطاقة بس عايز تدخل مكتب الوزير — الحارس يقولك “أنا عارفك بس ده مش مكانك”. فيه فرق كبير معمارياً: الـ 401 معناه إن الـ Client لازم يعيد تسجيل الدخول، أما الـ 403 فمعناه إن تسجيل الدخول مش هيحل المشكلة، أنت أصلاً مش مسموح ليك.

أخطاء شائعة#

الخطأ 1: رد بـ 200 مع Error في الـ Body:

// غلط — الـ Frontend هيفتكر الطلب نجح وهيحاول يقرأ الداتا!
res.statusCode = 200;
res.end(JSON.stringify({ error: 'User not found' }));

// صح — الـ Frontend هيدخل في الـ catch block فوراً
res.statusCode = 404;
res.end(JSON.stringify({ error: 'User not found' }));

الخطأ 2: رد بـ 500 على كل حاجة:

// غلط — مبتفرقش بين غلط المستخدم وغلط السيرفر
catch (err) {
    res.statusCode = 500;
    res.end(JSON.stringify({ error: err.message }));
}

// صح — فرّق بين الأنواع
catch (err) {
    if (err.name === 'ValidationError') {
        res.statusCode = 400; // ده غلط المستخدم، مش غلط السيرفر
    } else {
        res.statusCode = 500; // ده غلط فينا — Bug في الكود
    }
    res.end(JSON.stringify({ error: err.message }));
}

7. التحدي الكبير: ابني CRUD API كامل من الصفر#

دلوقتي هنجمع كل اللي اتعلمناه في مشروع واحد: API كامل لإدارة المستخدمين بدون أي مكتبة خارجية — http module فقط.

الهيكل الكامل:#

const http = require('http');
const url = require('url');

// ============ "قاعدة بياناتنا" المؤقتة في الميموري ============
let users = [
    { id: 1, name: 'أحمد محمد', email: 'ahmed@example.com', role: 'admin' },
    { id: 2, name: 'سارة علي', email: 'sara@example.com', role: 'user' },
    { id: 3, name: 'محمد خالد', email: 'mohammed@example.com', role: 'user' },
];
let nextId = 4;

// ============ Helper: قراءة الـ Body ============
function parseBody(req) {
    return new Promise((resolve, reject) => {
        if (req.method === 'GET' || req.method === 'DELETE') return resolve(null);
        
        let body = '';
        const MAX_SIZE = 1 * 1024 * 1024;
        let size = 0;

        req.on('data', (chunk) => {
            size += chunk.length;
            if (size > MAX_SIZE) { req.destroy(); return reject(new Error('Payload too large')); }
            body += chunk.toString();
        });
        req.on('end', () => {
            if (!body) return resolve(null);
            try { resolve(JSON.parse(body)); }
            catch { reject(new Error('Invalid JSON')); }
        });
        req.on('error', reject);
    });
}

// ============ Helper: استخراج الـ ID من المسار ============
// المسار: /users/5 → نرجع 5
function extractId(pathname) {
    const parts = pathname.split('/').filter(Boolean); // ['users', '5']
    if (parts.length === 2 && parts[0] === 'users') {
        const id = parseInt(parts[1], 10);
        return isNaN(id) ? null : id;
    }
    return null;
}

// ============ Helper: إرسال الرد ============
function sendResponse(res, statusCode, data) {
    res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify(data));
}

// ============ الـ Request Handler الرئيسي ============
async function handleRequest(req, res) {
    const parsedUrl = url.parse(req.url, true);
    const pathname = parsedUrl.pathname;
    const method = req.method;
    const query = parsedUrl.query;

    try {
        // =================== GET /users ===================
        // جيب كل المستخدمين (مع Pagination بسيطة)
        if (pathname === '/users' && method === 'GET') {
            const page = parseInt(query.page) || 1;
            const limit = parseInt(query.limit) || 10;
            const start = (page - 1) * limit;
            const paginatedUsers = users.slice(start, start + limit);

            return sendResponse(res, 200, {
                data: paginatedUsers,
                total: users.length,
                page,
                totalPages: Math.ceil(users.length / limit),
            });
        }

        // =================== GET /users/:id ===================
        // جيب مستخدم واحد بالـ ID
        if (pathname.startsWith('/users/') && method === 'GET') {
            const id = extractId(pathname);
            if (!id) return sendResponse(res, 400, { error: 'Invalid user ID' });

            const user = users.find(u => u.id === id);
            if (!user) return sendResponse(res, 404, { error: `المستخدم رقم ${id} مش موجود` });

            return sendResponse(res, 200, { data: user });
        }

        // =================== POST /users ===================
        // أنشئ مستخدم جديد
        if (pathname === '/users' && method === 'POST') {
            const body = await parseBody(req);

            // Validation — متثقش في أي داتا جاية من بره
            if (!body || !body.name || !body.email) {
                return sendResponse(res, 400, { error: 'الاسم والإيميل مطلوبين!' });
            }

            // تأكد إن الإيميل مش متكرر
            if (users.find(u => u.email === body.email)) {
                return sendResponse(res, 409, { error: 'الإيميل ده مسجل قبل كده!' });
            }

            const newUser = {
                id: nextId++,
                name: body.name,
                email: body.email,
                role: body.role || 'user',
            };
            users.push(newUser);

            return sendResponse(res, 201, { message: 'تم إنشاء المستخدم بنجاح', data: newUser });
        }

        // =================== PUT /users/:id ===================
        // عدّل مستخدم موجود (تعديل كامل)
        if (pathname.startsWith('/users/') && method === 'PUT') {
            const id = extractId(pathname);
            if (!id) return sendResponse(res, 400, { error: 'Invalid user ID' });

            const body = await parseBody(req);
            if (!body || !body.name || !body.email) {
                return sendResponse(res, 400, { error: 'الاسم والإيميل مطلوبين للتعديل!' });
            }

            const index = users.findIndex(u => u.id === id);
            if (index === -1) return sendResponse(res, 404, { error: `المستخدم رقم ${id} مش موجود` });

            // تأكد إن الإيميل الجديد مش بتاع حد تاني
            const emailTaken = users.find(u => u.email === body.email && u.id !== id);
            if (emailTaken) return sendResponse(res, 409, { error: 'الإيميل ده مسجل لمستخدم تاني!' });

            users[index] = { ...users[index], name: body.name, email: body.email, role: body.role || users[index].role };

            return sendResponse(res, 200, { message: 'تم التعديل بنجاح', data: users[index] });
        }

        // =================== DELETE /users/:id ===================
        // امسح مستخدم
        if (pathname.startsWith('/users/') && method === 'DELETE') {
            const id = extractId(pathname);
            if (!id) return sendResponse(res, 400, { error: 'Invalid user ID' });

            const index = users.findIndex(u => u.id === id);
            if (index === -1) return sendResponse(res, 404, { error: `المستخدم رقم ${id} مش موجود` });

            const deleted = users.splice(index, 1)[0];

            return sendResponse(res, 200, { message: `تم حذف "${deleted.name}" بنجاح` });
        }

        // =================== 404 — Route مش موجود ===================
        sendResponse(res, 404, { error: 'الـ Endpoint ده مش موجود' });

    } catch (err) {
        // Global Error Handler — أخطاء مش متوقعة
        console.error('Server Error:', err);
        sendResponse(res, err.message.includes('JSON') || err.message.includes('Payload') ? 400 : 500, {
            error: err.message.includes('JSON') || err.message.includes('Payload')
                ? err.message
                : 'حصل خطأ في السيرفر!',
        });
    }
}

// ============ تشغيل السيرفر ============
const PORT = process.env.PORT || 3000;
const server = http.createServer(handleRequest);

server.listen(PORT, () => {
    console.log(`
    ======================================
       السيرفر شغال على http://localhost:${PORT}
    ======================================
    الـ Endpoints المتاحة:
      GET    /users          → كل المستخدمين
      GET    /users/:id      → مستخدم واحد
      POST   /users          → إنشاء مستخدم
      PUT    /users/:id      → تعديل مستخدم
      DELETE /users/:id      → حذف مستخدم
    ======================================
    `);
});

تعال نجربه — الأوامر بتاعت الـ Terminal:#

# 1. جيب كل المستخدمين
curl http://localhost:3000/users

# 2. جيب مستخدم واحد
curl http://localhost:3000/users/1

# 3. أنشئ مستخدم جديد
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "علي حسن", "email": "ali@example.com"}'

# 4. عدّل مستخدم
curl -X PUT http://localhost:3000/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "أحمد محمد علي", "email": "ahmed.updated@example.com"}'

# 5. امسح مستخدم
curl -X DELETE http://localhost:3000/users/2

# 6. جرب Route مش موجود
curl http://localhost:3000/products

# 7. جرب JSON مكسور
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{invalid json}'

تحليل معماري — ليه الكود اتكتب بالشكل ده#

القرار المعماريالسبب
parseBody كـ Promiseعشان نقدر نستخدم async/await ونخلي الكود مقروء بدل الـ callback hell
sendResponse Helperعشان نضمن إن كل رد بيتبعت بنفس الـ Headers والصيغة — مفيش مكان ينسى الـ Content-Type
extractId Helperعشان نعمل الـ Parsing مرة واحدة ونتعامل مع الحالات الغريبة (مسار فاضي، ID مش رقم)
try/catch شاملأي Error غير متوقع يتمسك في الـ Global Handler ومبيوقعش السيرفر كله
التحقق من الإيميل المتكررده مثال على Business Logic — القواعد اللي أنت مسؤول عنها كمهندس Backend
الـ Paginationمتبعتش كل الداتا مرة واحدة! (فاكر كارثة الميموري من اليوم الأول؟)

8. التشبيه الكبير: ورشة النجارة#

عشان تبني Mental Model يكمل معاك، تخيل الموضوع كله كورشة نجارة:

  • الـ http module (المطرقة والمنشار اليدوي): الأدوات الأساسية الخام. بتشتغل بيها في كل حاجة بس محتاج مجهود. لو فهمتها، تقدر تبني أي حاجة من أي خشبة.

  • Express (المنشار الكهربايي): نفس المنشار بس بيدور لوحده. أسرع 10 مرات وأريح. بس لو باظ مش هتعرف تصلحه إلا لو فاهم مبادئ النشر.

  • الـ Request (الأوردر من العميل): “عايز ترابيزة 80×120، لون بني، 4 أرجل”. ده ورقة فيها: إيه عايز (Method)، إيه الشكل (URL/Path)، إيه المواصفات (Body)، ومعلومات إضافية (Headers).

  • الـ Response (المنتج النهائي): إنت بتطلع للعميل “الترابيزة” (الـ Body) ومعاها “فاتورة” (الـ Headers) و”تقييم الحالة” (الـ Status Code: 200 = تمام، 404 = مش لاقي الموديل).

  • الـ Router (ورق التوزيع): الأوردرات بتوصل كلها على مكتب واحد. إنت بتفرزهم: “الترابيزات في القسم A، والكراسي في القسم B، وأي حاجة مش عارفها أقوله مش موجودة”.

  • الـ Body Parsing (فتح صندوق المواصفات): العميل بعتلك صندوق فيه التفاصيل. إنت لازم تفتحه حتة حتة (Stream)، تتأكد إن مفيهوش قنبلة (Security)، وتقرأ الورقة اللي جواه (JSON.parse).


9. ساحة الاختبار (The Crucible)#

أ) أسئلة الانترفيو القاتلة#

  1. لغز الـ res.end() المنسي: لو الـ Handler بتاعك فيه condition معقد وفي أحد الـ branches نسيت تنادي res.end() — إيه اللي هيحصل بالظبط للمستخدم وللسيرفر؟

    إجابة: المتصفح هيفضل يلف (Spinner) مستني الرد لحد ما يجيله Timeout (المتصفح عادةً بينتظر 30-120 ثانية). السيرفر من ناحيته الـ Socket هيفضل مفتوح شايل الـ Connection — وده بيستهلك Resources. لو ده حصل مع 1000 Request، السيرفر هيوصل لحد الـ Maximum Open Connections ويقفل في وش أي طلب جديد. الحل: دايماً اتأكد إن كل path في الكود بتاعك بينتهي بـ res.end() — حتى الـ Error paths. في الـ Production، بنستخدم Timeout Middleware بيقفل الاتصال تلقائياً بعد مدة معينة عشان يحمي السيرفر.

  2. لغز الـ Headers: لو حاولت تعمل res.setHeader('X-Custom', 'value') بعد ما عملت res.write('data') — هيحصل إيه؟ وليه؟

    إجابة: Node هيرمي Error: Cannot set headers after they are sent to the client. السبب إن بروتوكول HTTP بيقول إن الـ Headers لازم تتبعت قبل الـ Body. التنسيق الفعلي للرد هو: Status Line → Headers → Empty Line → Body. لما عملت res.write() الأول، Node بعت الـ Headers اللي كانت متضبطة حتى اللحظة دي + الـ Body. مينفعش ترجع تاني تزود headers. الدرس: خطط للـ Headers بتاعتك قبل ما تبدأ تبعت أي Body.

  3. سؤال الـ Streams: لو عندك Endpoint بيرجع ملف ضخم (مثلاً CSV 2GB) — هل تستخدم res.end(fileContent) ولا في طريقة أحسن؟

    إجابة: مستحيل تستخدم res.end(fileContent) لأن ده معناه إنك هتقرأ الـ 2GB كلهم في الـ RAM الأول وبعدين تبعتهم — السيرفر هيقع. الطريقة الصح: استخدم Streams. fs.createReadStream('big-file.csv').pipe(res). كده Node هيقرأ حتة من الملف (مثلاً 64KB)، يبعتها للمتصفح فوراً، ويمسحها من الـ RAM ويجيب اللي بعدها. بالطريقة دي تقدر تبعت ملف 100GB وإنت شغال بـ 50MB RAM بس. والـ pipe كمان بيتعامل مع Backpressure تلقائياً: لو المتصفح بطيء في الاستقبال، الـ Stream بيبطئ في القراءة عشان الـ Buffer ميتملاش.

ب) سيناريو War Room#

“السيرفر بتاعنا (Raw Node.js HTTP) بيقع كل يوم الساعة 3 الفجر. الـ Monitoring بيقول إن الـ RAM بتوصل لـ 100% وبيعمل OOM Kill. بالنهار مفيش مشكلة.”

تحليل: الـ 3 الفجر = غالباً Cron Job أو Scheduled Task بيشتغل في الوقت ده (مثلاً Export كبير أو Report Generation). الكود غالباً بيقرأ داتا ضخمة من الداتابيز ويحطها كلها في متغير واحد في الـ RAM (زي const allData = await db.query('SELECT * FROM logs') — لو الـ logs مليون record، ده ممكن ياخد 2GB RAM). الحل: (1) حول لـ Streaming: بدل ما تسحب كل الـ data مرة واحدة، استخدم Cursor/Stream من الداتابيز واكتب في الـ Response أو الملف حتة بحتة. (2) حط Memory Limits بالـ --max-old-space-size. (3) اعمل Monitoring Alerts لما الـ RAM توصل 80% بدل ما تستنى 100% واللي كان كان.


10. خلاصة اليوم — الـ Mental Model الكامل#

من الآخر: النهاردة فهمنا إن Express والفريم ووركات التانية مجرد طبقة تجميل فوق الـ http module الأصلي. الـ HTTP في جوهره بسيط: طلب (Request) فيه Method و Path و Headers و Body، ورد (Response) فيه Status Code و Headers و Body. كل الـ Routing الفخم بتاع Express تحته مجرد مقارنة Strings. وكل الـ Body Parsing تحته مجرد تجميع Chunks من Stream. لما تفهم الأساس، أي Framework يبقى مجرد أداة — مش سحر.

في اليوم الجاي هنبدأ نرتب الكود بتاعنا في هيكل مشروع محترم (Project Structure & Modules) — عشان الكود اللي كتبناه النهاردة في ملف واحد مستحيل يعيش في Production.


Join our whatsapp group here
My Channel here