http.createServer() יוצר שרת web מפונקציה אחת, ה-request handler, ש-Node קורא לה לכל בקשה נכנסת עם שני אובייקטים: req (ה-method, ה-URL וה-headers שהלקוח שלח) ו-res (מה שאתם שולחים בחזרה). הגדירו קוד סטטוס ו-headers ב-res, ואז קראו ל-res.end() עם הגוף. server.listen(3000) מתחיל לקבל חיבורים ב-port 3000.
הבקשה הראשונה עונה You asked for GET /hello?name=Ada, וה-Console מדפיס את אותם method ו-URL. שלחו DELETE /anything משורת הבקשה: אותו handler עונה עליה, כי לשרת http רגיל אין routes. כל בקשה עוברת דרך הפונקציה האחת.
שליחת JSON והגדרת קוד הסטטוס
res.writeHead(status, headers) מגדיר את קוד הסטטוס ואת ה-headers בקריאה אחת. בשביל JSON, הגדירו Content-Type: application/json ושלחו את התוצאה של JSON.stringify(); res.end() מקבל רק מחרוזת או Buffer.
GET /users?limit=1 מחזיר [{"id":1,"name":"Ada"}]. req.url הוא הנתיב ועוד ה-query string, ולכן new URL(req.url, 'http://localhost') מפצל אותו ל-pathname ול-searchParams (הבסיס נמצא שם רק כי URL צריך כתובת מוחלטת). שלחו GET /nothing והסטטוס יהפוך ל-404.
ניתוב בקשות עם switch
ל-API קטן לא צריך יותר מ-switch על ה-method והנתיב. בנו מפתח כמו 'GET /users' ותנו לכל route case משלו:
שלחו POST /users עם הגוף {"name": "Grace"}: התשובה היא 201 עם {"id":2,"name":"Grace"}, ו-GET /users מציג עכשיו את שני המשתמשים. שלחו את אותו POST עם הגוף hello ותקבלו 400 עם {"error":"Body must be JSON"}.
קריאת גוף של POST
Node לא מפענח את הגוף בשבילכם. req הוא readable stream, והגוף מגיע בחלקים דרך אירועי data, ואז end נפלט כשהוא שלם. readBody() שלמעלה אוסף את החלקים למחרוזת ומחזיר promise, כך שה-handler יכול לעשות לו await. מכיוון ש-req הוא async iterable, הלולאה הזו עושה את אותה עבודה:
let data = '';
for await (const chunk of req) data += chunk;
const body = JSON.parse(data);
שרת אמיתי גם מגביל את הגודל (מפסיק לקרוא ועונה 413 ברגע ש-data.length עובר מגבלה) ובודק את req.headers['content-type'] לפני הקריאה ל-JSON.parse.
טעויות נפוצות
לשכוח את res.end(). ענף שמגדיר סטטוס אבל אף פעם לא מסיים את התשובה משאיר את הלקוח מחכה עד ל-timeout.
הגדרת header אחרי הגוף. ברגע ש-res.end() או ה-res.write() הראשון רצו, ה-headers נשלחו, ו-res.setHeader() זורק ERR_HTTP_HEADERS_SENT. השתמשו ב-return כשאתם שולחים תשובה מוקדם, כמו בדוגמאות.
השוואה של req.url לנתיב. req.url כולל את ה-query string, ולכן req.url === '/users' הוא false עבור /users?limit=1. השוו את pathname.
מתי לעבור ל-Express
המודול http מספיק ל-health check, למקבל webhooks או כדי ללמוד איך HTTP עובד. ברגע שצריך פרמטרים ב-route כמו /users/:id, פענוח גוף JSON, middleware וטיפול בשגיאות, בסוף כותבים framework קטן משלכם. Express הוא ה-framework הזה, בנוי על אותם אובייקטי req ו-res; ניתוב ב-Express מראה את אותו API עם app.get() ו-app.post().
שאלות נפוצות
מה ההבדל בין res.write ל-res.end?
res.write() שולח חלק מהגוף ומשאיר את התשובה פתוחה, כך שאפשר לקרוא לו הרבה פעמים. res.end() שולח חלק אחרון אופציונלי ומסיים את התשובה. כל תשובה צריכה בדיוק res.end() אחד.
איך עוצרים שרת HTTP של Node?
קוראים ל-server.close(). הוא מפסיק מיד לקבל חיבורים חדשים ומריץ את ה-callback שלו כשהחיבורים הפתוחים הסתיימו, וזה בדיוק מה שכיבוי נקי ב-SIGTERM צריך.
איך משתמשים ב-HTTPS עם המודול http?
משתמשים במודול https במקום: https.createServer({ key, cert }, handler) מקבל את אותו handler ועוד מפתח פרטי ותעודה. ב-production בדרך כלל proxy כמו Nginx או פלטפורמת האירוח מטפלים ב-TLS, ושרת ה-Node שלכם ממשיך להאזין ב-HTTP רגיל.
על איזה port שרת Node צריך להאזין?
כל port פנוי מעל 1024 עובד בלי הרשאות מנהל; 3000 ו-8080 נפוצים בפיתוח. פלטפורמות אירוח בדרך כלל מעבירות את ה-port במשתנה סביבה, אז כתבו server.listen(process.env.PORT || 3000).