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 ומחזיר Promiseresponse.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()- מחזיר ArrayBufferresponse.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.stringifyContent-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 צריך התקנה