לדלג לתוכן

4.6 בקשות HTTP הרצאה

מה זה API ו-REST

לפני שנלמד איך לשלוח בקשות HTTP מג'אווהסקריפט, בואו נבין מה זה API.

  • API (Application Programming Interface) - ממשק שמאפשר לתוכנות לתקשר ביניהן
  • REST (Representational State Transfer) - סגנון ארכיטקטורה לבניית API-ים על גבי HTTP
  • ב-REST, כל משאב (resource) מיוצג על ידי URL, ואנחנו משתמשים בפעולות HTTP כדי לטפל בו

הפעולות העיקריות:
- GET - קריאת מידע
- POST - יצירת מידע חדש
- PUT - עדכון מלא של מידע קיים
- PATCH - עדכון חלקי
- DELETE - מחיקת מידע

לדוגמה, API של חנות:
- GET /products - קבלת כל המוצרים
- GET /products/5 - קבלת מוצר מספר 5
- POST /products - יצירת מוצר חדש
- DELETE /products/5 - מחיקת מוצר מספר 5


ה-Fetch API

ב-JS מודרני, הדרך לשלוח בקשות HTTP היא עם fetch. זו פונקציה גלובלית שמגיעה מובנית בדפדפן.

  • fetch מחזיר Promise
  • אנחנו כבר יודעים לעבוד עם Promises ו-async/await, אז זה ישתלב יפה

בקשת GET בסיסית

fetch("https://jsonplaceholder.typicode.com/posts/1")
    .then(response => response.json())
    .then(data => {
        console.log(data);
        // { userId: 1, id: 1, title: "...", body: "..." }
    })
    .catch(error => {
        console.error("Request failed:", error);
    });
  • fetch(url) - שולח בקשת GET ל-URL ומחזיר Promise
  • response.json() - מפענח את גוף התשובה מ-JSON לאובייקט JS (גם זה מחזיר Promise!)
  • .catch - תופס שגיאות רשת (אבל לא שגיאות 404 או 500 - נסביר בהמשך)

אובייקט התשובה - Response

כשה-Promise של fetch מתממש, אנחנו מקבלים אובייקט Response עם מידע שימושי:

fetch("https://jsonplaceholder.typicode.com/posts/1")
    .then(response => {
        console.log(response.status);     // 200
        console.log(response.ok);         // true (status 200-299)
        console.log(response.statusText); // "OK"
        console.log(response.headers);    // Headers object
        console.log(response.url);        // the final URL (after redirects)

        return response.json(); // parse body as JSON
    });

שיטות לקריאת גוף התשובה

  • response.json() - מפענח JSON (הכי נפוץ)
  • response.text() - מחזיר טקסט רגיל
  • response.blob() - מחזיר Blob (לקבצים בינאריים כמו תמונות)
  • response.arrayBuffer() - מחזיר ArrayBuffer
  • response.formData() - מחזיר FormData

חשוב: אפשר לקרוא את הגוף רק פעם אחת! אחרי שקראתם response.json(), לא תוכלו לקרוא שוב.

// reading response as text (for example HTML or plain text)
fetch("https://example.com")
    .then(response => response.text())
    .then(html => {
        console.log(html); // "<!DOCTYPE html>..."
    });

בקשת POST

כשרוצים לשלוח מידע לשרת (ליצור משאב חדש), משתמשים ב-POST:

fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({
        title: "My New Post",
        body: "This is the content",
        userId: 1
    })
})
    .then(response => response.json())
    .then(data => {
        console.log("Created:", data);
    });
  • method: "POST" - מגדיר את סוג הבקשה
  • headers - מגדיר את ה-headers, כולל סוג התוכן
  • body - גוף הבקשה. חייבים להמיר אובייקט JS ל-JSON עם JSON.stringify
  • Content-Type: application/json - אומר לשרת שאנחנו שולחים JSON

פעולות PUT, PATCH ו-DELETE

// PUT - update entire resource
fetch("https://jsonplaceholder.typicode.com/posts/1", {
    method: "PUT",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({
        id: 1,
        title: "Updated Title",
        body: "Updated content",
        userId: 1
    })
})
    .then(response => response.json())
    .then(data => console.log("Updated:", data));

// PATCH - partial update
fetch("https://jsonplaceholder.typicode.com/posts/1", {
    method: "PATCH",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({
        title: "Only changing the title"
    })
})
    .then(response => response.json())
    .then(data => console.log("Patched:", data));

// DELETE
fetch("https://jsonplaceholder.typicode.com/posts/1", {
    method: "DELETE"
})
    .then(response => {
        if (response.ok) {
            console.log("Deleted successfully");
        }
    });
  • PUT - מחליף את כל המשאב. צריך לשלוח את כל השדות
  • PATCH - מעדכן רק את השדות ששלחנו
  • DELETE - לא צריך body בדרך כלל

פרמטרים בכתובת - Query Parameters

הרבה API-ים משתמשים ב-query parameters כדי לסנן, לחפש או לעמד תוצאות:

// the manual way - string concatenation
const query = "javascript";
const page = 2;
fetch(`https://api.example.com/search?q=${query}&page=${page}`);

אבל הדרך הנכונה היא להשתמש ב-URLSearchParams:

// the proper way - URLSearchParams
const params = new URLSearchParams({
    q: "javascript",
    page: 2,
    limit: 10
});

fetch(`https://api.example.com/search?${params}`);
// URL: https://api.example.com/search?q=javascript&page=2&limit=10

// URLSearchParams handles encoding special characters
const params2 = new URLSearchParams({
    q: "hello world & more",
    category: "books/fiction"
});
console.log(params2.toString());
// "q=hello+world+%26+more&category=books%2Ffiction"
  • URLSearchParams מקודד תווים מיוחדים אוטומטית
  • אפשר גם להוסיף פרמטרים אחד-אחד עם params.append("key", "value")
  • אפשר לקבל ערך עם params.get("key")

טיפול בשגיאות - הטעות הנפוצה ביותר

זה הדבר הכי חשוב להבין לגבי fetch:

fetch לא זורק שגיאה על תשובות 4xx או 5xx!

fetch זורק שגיאה רק כשיש בעיית רשת (אין אינטרנט, השרת לא מגיב, DNS נכשל). אם השרת החזיר 404 או 500, fetch מחשיב את זה כהצלחה כי הוא קיבל תשובה.

// BAD - this won't catch 404 errors!
fetch("https://api.example.com/nonexistent")
    .then(response => response.json()) // this runs even on 404!
    .then(data => console.log(data))
    .catch(error => console.error(error)); // only network errors

// GOOD - check response.ok
fetch("https://api.example.com/nonexistent")
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.error("Error:", error.message));
  • response.ok הוא true רק כשה-status בין 200 ל-299
  • תמיד בדקו את response.ok לפני שמפענחים את התשובה
  • זו טעות מאוד נפוצה בקרב מפתחים חדשים

השוואה לפייתון

בפייתון, ספריית requests מתנהגת דומה - גם היא לא זורקת שגיאה על 4xx/5xx אלא אם קוראים ל-raise_for_status():

# import requests
# response = requests.get("https://api.example.com/nonexistent")
# response.raise_for_status()  # raises HTTPError on 4xx/5xx

async/await עם fetch

הדרך הנקייה והמומלצת לעבוד עם fetch:

async function getPost(id) {
    try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Failed to fetch post:", error.message);
        throw error; // re-throw so the caller knows it failed
    }
}

// usage
async function main() {
    const post = await getPost(1);
    console.log(post.title);
}

main();
  • שימו לב שresponse.json() גם מחזיר Promise, אז צריך await גם עליו
  • try/catch תופס גם שגיאות רשת וגם את ה-throw שלנו על response.ok

בקשות מקבילות

async function getAllData() {
    // run requests in parallel
    const [usersRes, postsRes] = await Promise.all([
        fetch("https://jsonplaceholder.typicode.com/users"),
        fetch("https://jsonplaceholder.typicode.com/posts")
    ]);

    const users = await usersRes.json();
    const posts = await postsRes.json();

    console.log(`${users.length} users, ${posts.length} posts`);
}

ביטול בקשות - AbortController

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

const controller = new AbortController();

fetch("https://jsonplaceholder.typicode.com/posts", {
    signal: controller.signal
})
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => {
        if (error.name === "AbortError") {
            console.log("Request was cancelled");
        } else {
            console.error("Request failed:", error);
        }
    });

// cancel the request
controller.abort();
  • יוצרים AbortController ומעבירים את ה-signal שלו ל-fetch
  • קריאה ל-controller.abort() מבטלת את הבקשה
  • הביטול זורק שגיאה מסוג AbortError

טיימאאוט - Timeout עם AbortController

fetch לא תומך ב-timeout באופן מובנה, אבל אפשר לבנות אחד עם AbortController:

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();

    // cancel request after timeout
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal
        });

        clearTimeout(timeoutId); // request finished before timeout
        return response;
    } catch (error) {
        clearTimeout(timeoutId);
        if (error.name === "AbortError") {
            throw new Error(`Request timed out after ${timeout}ms`);
        }
        throw error;
    }
}

// usage - timeout after 3 seconds
const response = await fetchWithTimeout(
    "https://api.example.com/slow-endpoint",
    {},
    3000
);

CORS - שיתוף משאבים בין מקורות

CORS (Cross-Origin Resource Sharing) הוא מנגנון אבטחה בדפדפן.

  • הדפדפן חוסם בקשות מ-origin אחד ל-origin שונה
  • Origin = protocol + domain + port (למשל https://mysite.com:443)
  • אם האתר שלכם ב-localhost:3000 ואתם שולחים בקשה ל-api.example.com, הדפדפן יחסום את זה
  • זה קורה רק בדפדפן! שרת יכול לשלוח בקשות לכל מקום
Access to fetch at 'https://api.example.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy
  • הפתרון: השרת צריך להוסיף header של Access-Control-Allow-Origin
  • בפיתוח מקומי: אפשר להשתמש ב-proxy דרך כלי כמו Vite
  • חלק מה-API-ים הציבוריים (כמו jsonplaceholder) כבר מאפשרים CORS

כותרות - Headers

כותרות HTTP הן מטא-דאטה שנשלחת עם הבקשה והתשובה:

fetch("https://api.example.com/data", {
    headers: {
        "Content-Type": "application/json",  // format of data we're sending
        "Accept": "application/json",         // format we want back
        "Authorization": "Bearer eyJhbGci..." // authentication token
    }
});

כותרות נפוצות

  • Content-Type - סוג התוכן שאנחנו שולחים (JSON, form data, וכו')
  • Accept - סוג התוכן שאנחנו רוצים לקבל
  • Authorization - אימות (בדרך כלל Bearer token)

קריאת כותרות מהתשובה

const response = await fetch("https://api.example.com/data");

console.log(response.headers.get("Content-Type"));    // "application/json"
console.log(response.headers.get("X-Total-Count"));   // custom header

// iterate over all headers
for (const [key, value] of response.headers) {
    console.log(`${key}: ${value}`);
}

שליחת קבצים עם FormData

כדי להעלות קבצים, משתמשים ב-FormData:

// with a file input element
const fileInput = document.querySelector("#fileInput");

const formData = new FormData();
formData.append("file", fileInput.files[0]);
formData.append("description", "My uploaded file");

fetch("https://api.example.com/upload", {
    method: "POST",
    body: formData
    // don't set Content-Type! The browser sets it automatically
    // with the correct boundary for multipart/form-data
});
  • אל תגדירו Content-Type כששולחים FormData - הדפדפן מגדיר את זה אוטומטית עם boundary נכון
  • אפשר להוסיף גם שדות טקסט רגילים ל-FormData
// creating FormData from a form element
const form = document.querySelector("#myForm");
const formData = new FormData(form); // automatically includes all form fields

fetch("https://api.example.com/submit", {
    method: "POST",
    body: formData
});

פונקציית עטיפה - Reusable Fetch Wrapper

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

const API_BASE = "https://api.example.com";

async function api(endpoint, options = {}) {
    const { method = "GET", body, headers = {}, timeout = 10000 } = options;

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
        const config = {
            method,
            signal: controller.signal,
            headers: {
                "Content-Type": "application/json",
                "Accept": "application/json",
                ...headers
            }
        };

        if (body) {
            config.body = JSON.stringify(body);
        }

        const response = await fetch(`${API_BASE}${endpoint}`, config);

        clearTimeout(timeoutId);

        if (!response.ok) {
            const errorData = await response.json().catch(() => ({}));
            const error = new Error(errorData.message || `HTTP ${response.status}`);
            error.status = response.status;
            error.data = errorData;
            throw error;
        }

        // handle empty responses (204 No Content)
        if (response.status === 204) {
            return null;
        }

        return await response.json();
    } catch (error) {
        clearTimeout(timeoutId);

        if (error.name === "AbortError") {
            throw new Error("Request timed out");
        }

        throw error;
    }
}

// usage examples
async function main() {
    // GET
    const posts = await api("/posts");

    // GET with query params
    const params = new URLSearchParams({ page: 1, limit: 10 });
    const users = await api(`/users?${params}`);

    // POST
    const newPost = await api("/posts", {
        method: "POST",
        body: { title: "Hello", content: "World" }
    });

    // DELETE
    await api("/posts/1", { method: "DELETE" });

    // with auth token
    const profile = await api("/me", {
        headers: {
            "Authorization": "Bearer my-token-here"
        }
    });
}

הפונקציה הזו מטפלת ב:
- URL בסיס קבוע
- המרה אוטומטית ל-JSON
- בדיקת response.ok
- טיימאאוט
- כותרות ברירת מחדל
- טיפול בתשובות ריקות (204)


השוואה לפייתון - ספריית requests

בפייתון השתמשנו ב-requests שעובדת באופן סינכרוני:

# import requests
#
# # GET
# response = requests.get("https://api.example.com/posts")
# data = response.json()
#
# # POST
# response = requests.post("https://api.example.com/posts", json={"title": "Hello"})
#
# # check status
# response.raise_for_status()  # throws on 4xx/5xx

הבדלים מ-fetch:
- requests היא סינכרונית, fetch היא אסינכרונית (מחזירה Promise)
- requests.post(url, json=data) - לא צריך JSON.stringify ידנית
- raise_for_status() דומה לבדיקת response.ok שלנו
- אין CORS בפייתון כי זה רץ בשרת, לא בדפדפן
- fetch מגיע מובנה בדפדפן, requests צריך התקנה