דלג לתוכן
Full Stack / מדריך

מהדפדפן לשרת: בונים טופס שמירה ורואים מה קורה בדרך

טופס HTML, בקשת POST ושרת Node.js קטן. דוגמה מלאה שממחישה איך נתונים עוברים ואיפה בודקים אותם.

11.07.2026 · 9 דקות קריאה · למתחילים
טופס בדפדפן שולח מעטפת נתונים לשרת ומשם למסד הנתונים

איור: HomeRan

תוכן עניינים

לחיצה על ״שמירה״ מתחילה בממשק, אבל המידע צריך להגיע למקום שיודע לבדוק ולשמור אותו. בדרך יש בקשה, כתובת, גוף נתונים ותשובה. כשבונים דוגמה קטנה בלי framework (תשתית קוד מוכנה לבניית אפליקציות), קל לראות מה כל חלק עושה.

נכין טופס ששומר כותרת של פוסט. השרת יחזיר את הפוסט שנוצר, והממשק יציג אישור. השמירה בדוגמה היא בזיכרון בלבד: המידע נעלם כשעוצרים את השרת. זה מאפשר להתרכז במסלול הבקשה לפני שמוסיפים מסד נתונים.

מה צריך?

  • Node.js 22 ומעלה, זמין דרך הפקודה node.
  • דפדפן מודרני.
  • תיקייה חדשה עם שני קבצים: server.mjs ו־index.html.

אין צורך להתקין חבילות. הדוגמה משתמשת במודול HTTP המובנה של Node.js וב־Fetch של הדפדפן.

השרת: לקבל, לבדוק ולענות

שמרו את הקוד הבא ב־server.mjs:

JavaScript
import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import { randomUUID } from "node:crypto";

const html = readFileSync(new URL("./index.html", import.meta.url));
const posts = [];
const maxBodyBytes = 16 * 1024;

function sendJson(res, status, data) {
  res.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8"
  });
  res.end(JSON.stringify(data));
}

const server = createServer((req, res) => {
  if (req.method === "GET" && req.url === "/") {
    res.writeHead(200, {
      "Content-Type": "text/html; charset=utf-8"
    });
    res.end(html);
    return;
  }

  if (req.method === "GET" && req.url === "/api/posts") {
    sendJson(res, 200, { posts });
    return;
  }

  if (req.method !== "POST" || req.url !== "/api/posts") {
    sendJson(res, 404, {
      error: { code: "NOT_FOUND", message: "הכתובת לא נמצאה" }
    });
    return;
  }

  const contentType = String(req.headers["content-type"] ?? "")
    .split(";")[0].trim().toLowerCase();

  if (contentType !== "application/json") {
    req.resume();
    sendJson(res, 415, {
      error: { code: "JSON_REQUIRED", message: "נדרש גוף JSON" }
    });
    return;
  }

  const chunks = [];
  let size = 0;
  let tooLarge = false;

  req.on("data", chunk => {
    if (tooLarge) return;
    size += chunk.length;

    if (size > maxBodyBytes) {
      tooLarge = true;
      chunks.length = 0;
      sendJson(res, 413, {
        error: { code: "BODY_TOO_LARGE", message: "הבקשה גדולה מדי" }
      });
      return;
    }

    chunks.push(chunk);
  });

  req.on("end", () => {
    if (tooLarge) return;

    let body;
    try {
      body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
    } catch {
      sendJson(res, 400, {
        error: { code: "INVALID_JSON", message: "ה־JSON אינו תקין" }
      });
      return;
    }

    const title = typeof body?.title === "string"
      ? body.title.trim() : "";

    if (!title || title.length > 120) {
      sendJson(res, 400, {
        error: {
          code: "INVALID_TITLE",
          message: "הכותרת צריכה להכיל 1-120 תווים"
        }
      });
      return;
    }

    const post = { id: randomUUID(), title };
    posts.push(post);
    sendJson(res, 201, { post });
  });

  req.on("error", () => {
    if (!res.headersSent) {
      sendJson(res, 400, {
        error: { code: "READ_FAILED", message: "קריאת הבקשה נכשלה" }
      });
    }
  });
});

server.listen(3000, "127.0.0.1", () => {
  console.log("Open http://127.0.0.1:3000");
});

השרת מכיר שלוש בקשות: הצגת הדף, קבלת רשימת הפוסטים ויצירת פוסט. ביצירה הוא בודק את סוג התוכן, מגביל את גודל הגוף, מפענח JSON ובודק את הכותרת. רק אחרי הבדיקות הוא מוסיף רשומה למערך.

קוד הסטטוס 201 מציין שנוצר משאב חדש. קוד 400 מציין בעיה בבקשת הלקוח. כאן משתמשים בו עבור JSON לא תקין או כותרת שאינה עומדת בכללים. אלו המשמעויות המתוארות בתיעוד MDN ל־201 ול־400.

הממשק: לשלוח ולהציג תוצאה

שמרו את הקוד הבא ב־index.html:

HTML
<!doctype html>
<html lang="he" dir="rtl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>שמירת פוסט</title>
</head>
<body>
  <h1>פוסט חדש</h1>
  <form id="post-form">
    <label for="title">כותרת</label>
    <input id="title" name="title" required maxlength="120">
    <button id="save" type="submit">שמירה</button>
  </form>
  <p id="message" role="status"></p>

  <script>
    const form = document.querySelector("#post-form");
    const input = document.querySelector("#title");
    const button = document.querySelector("#save");
    const message = document.querySelector("#message");

    form.addEventListener("submit", async event => {
      event.preventDefault();
      button.disabled = true;
      message.textContent = "שומר...";

      try {
        const response = await fetch("/api/posts", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ title: input.value })
        });

        const data = await response.json();

        if (!response.ok) {
          throw new Error(data.error?.message ?? "השמירה נכשלה");
        }

        message.textContent = `נשמר: ${data.post.title}`;
        form.reset();
      } catch (error) {
        message.textContent = error.message;
      } finally {
        button.disabled = false;
      }
    });
  </script>
</body>
</html>

הכותרת נשלחת כמחרוזת בתוך JSON. התוצאה מוצגת באמצעות textContent, כך שהכותרת נכתבת כטקסט ולא מתפרשת כ־HTML. השדה מתנקה רק אחרי הצלחה. כישלון משאיר את מה שהמשתמש כתב, כדי שיוכל לתקן או לנסות שוב.

הקוד מניח שהתשובות מהשרת הזה הן JSON, כפי שמוגדר בדוגמה. במערכת רחבה יותר גם שרת ביניים יכול להחזיר HTML או גוף ריק. המאמר על טיפול בשגיאות Fetch מטפל גם במקרים האלה.

פרסומת

מריצים ובודקים

מתוך התיקייה עם שני הקבצים, הריצו:

Shell
node server.mjs

פתחו בדפדפן את http://127.0.0.1:3000. אל תפתחו את קובץ ה־HTML ישירות: השרת מגיש את הדף ואת ה־API מאותו מקור, ולכן הכתובת היחסית /api/posts מגיעה אליו בלי צורך בהגדרות CORS (הכללים שמתירים לדף לפנות לשרת ממקור אחר).

הקלידו כותרת ולחצו על ״שמירה״. אמורה להופיע הודעה עם הכותרת שנשמרה. פתיחת http://127.0.0.1:3000/api/posts מציגה את הרשומות שהשרת מחזיק בזיכרון.

כעת פתחו את כלי הפיתוח של הדפדפן ובחרו בלשונית Network. שמרו פוסט נוסף וחפשו את הבקשה ל־/api/posts. אפשר לראות את שיטת POST, את הנתונים שנשלחו ואת תגובת 201 עם המזהה שנוצר.

למה בודקים גם בדפדפן וגם בשרת?

המאפיינים required ו־maxlength עוזרים למשתמש למלא את הטופס. הם אינם מחליפים בדיקה בשרת: לקוח אחר יכול לשלוח בקשה ישירות בלי להשתמש בטופס הזה.

אפשר לבדוק זאת מתוך ה־Console של הדף:

JavaScript
const response = await fetch("/api/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "   " })
});

console.log(response.status);
console.log(await response.json());

הסטטוס הוא 400, והתגובה מסבירה שהכותרת אינה תקינה. השרת מסיר את הרווחים עם trim, ולכן הם אינם נחשבים לכותרת.

השרת בדוגמה מאזין רק למחשב המקומי. הוא אינו כולל משתמשים, הרשאות או אחסון קבוע, ואינו מיועד להפעלה כאתר ציבורי. אחרי שמבינים את מסלול הבקשה, אפשר להוסיף מסד נתונים ולבדוק מה קורה כשהשמירה עצמה נכשלת.

לקריאה נוספת

פרסומת

עוד משהו לקרוא

לכל הכתבות בנושא

מה מעניין אותך?