GraphQL מול REST: מה כל אחד פותר בתכנון API, ומה המחיר האדריכלי שלו
GraphQL ו-REST אינם שתי דרכים שונות “לעשות אותו דבר”. הם פותרים בעיות תכנון שונות, ולכן גם יוצרים כשלים שונים. מי שבוחר ביניהם רק לפי נוחות בצד הלקוח, או לפי מה שנראה מודרני יותר, מגלה בדרך כלל מאוחר מדי שהבחירה משפיעה על גבולות שירותים, גרסאות, ניטור, אבטחה, ביצועים ואפילו על מבנה הצוותים.
בקטגוריית תכנון API, השאלה הטובה איננה “מה עדיף: GraphQL או REST”, אלא: איזה חוזה אנחנו רוצים להציע, למי, ובאיזה מחיר תפעולי? במערכת קטנה ההבדלים נראים אסתטיים. במערכת עם microservices architecture, לקוחות מובייל, דאשבורדים פנימיים ואינטגרציות חיצוניות — הם הופכים להחלטה אדריכלית.
כדי לדבר על זה באופן מעשי, נפריד בין שלושה דברים: הצורה שבה הלקוח מבקש מידע, הצורה שבה השרת מארגן אחריות, והדרך שבה שינוי מתגלגל לאורך זמן. באמצע נראה גם דוגמת Java קצרה שממחישה איך להסתיר את בחירת הפרוטוקול מאחורי ממשק תכנוני, במקום לתת לה לזלוג לכל שכבות הקוד.
מה REST פותר היטב בתכנון API
REST מתאים במיוחד כאשר המערכת בנויה סביב משאבים ברורים: משתמשים, הזמנות, מוצרים, חשבוניות, מסמכים. כל משאב מקבל כתובת, והפעולות עליו מבוטאות באמצעות סמנטיקה מוכרת של HTTP: קריאה, יצירה, עדכון, מחיקה, ולעיתים פעולות ייעודיות שנוספות בזהירות.
היתרון הגדול של REST הוא פשטות חוזית. כתובת כמו /products/42 אומרת משהו גם בלי מסמך ארוך. קל לשמור אותה במטמון, קל לבדוק אותה בלוגים, קל להסביר אותה לצוות אחר, וקל יחסית להגן עליה ברמת שער API.
בתכנון נכון, REST נותן:
- גבולות משאב ברורים — כל endpoint אחראי על מושג עסקי מוגדר.
- שימוש טבעי ב-HTTP — סטטוסים, headers, caching, conditional requests והרשאות.
- תאימות טובה לאינטגרציות חיצוניות — במיוחד כאשר הלקוח אינו בשליטתכם.
- ניטור פשוט יותר — אפשר לראות בקלות איזה endpoint איטי, נשבר או מנוצל לרעה.
אבל REST גם מכריח את המתכנן לקבל החלטות מוקדמות: מהו המשאב? כמה מידע endpoint מחזיר? האם מסך מוצר צריך לקבל גם מלאי, גם מחיר וגם ביקורות? אם כן, האם יוצרים endpoint מורכב כמו /product-pages/42, או נותנים ללקוח לקרוא שלושה endpoints?
מה GraphQL פותר היטב בתכנון API
GraphQL משנה את נקודת המבט. במקום שהשרת יחליט מראש אילו צורות תגובה קיימות, הוא מפרסם סכמה. הלקוח מבקש בדיוק את השדות שהוא צריך, במבנה שמתאים למסך או לתהליך שלו. זה שימושי במיוחד כאשר יש הרבה לקוחות שונים: אפליקציית מובייל, אתר, מערכת ניהול, אינטגרציות פנימיות ודוחות.
GraphQL פותר היטב שתי בעיות מוכרות:
- Over-fetching — הלקוח מקבל יותר מידע ממה שהוא צריך.
- Under-fetching — הלקוח צריך להריץ כמה קריאות כדי להרכיב מסך אחד.
במקום לקרוא ל-/products/42, ואז ל-/products/42/price, ואז ל-/products/42/reviews, לקוח GraphQL יכול לבקש מבנה אחד: מוצר, מחיר, דירוג, תמונה ושם ספק. מבחינת חוויית פיתוח בצד הלקוח זה לעיתים שינוי דרמטי.
המחיר הוא שהשרת נהיה חכם יותר. צריך לנהל סכמה, resolver-ים, הרשאות שדה, הגבלת עומק שאילתות, מניעת שאילתות יקרות, ניטור לפי פעולה לוגית ולא רק לפי URL, והבנה טובה של זרימת נתונים בין שירותים.
השוואה קצרה: לא לפי טעם, אלא לפי אחריות
| שאלה תכנונית | REST |
GraphQL |
|---|---|---|
| מי קובע את צורת התגובה? | השרת, דרך endpoint מוגדר | הלקוח, במסגרת סכמה מוגדרת |
| איך מתארים את המודל? | משאבים ופעולות HTTP | סוגים, שדות וקשרים |
| מה קל לנטר? | URL, סטטוס, latency לפי endpoint | שם פעולה, עומק שאילתה, שדות יקרים |
| איפה מסתתרת מורכבות? | ריבוי endpoints או endpoints מותאמי מסך | resolver-ים, הרשאות, N+1, עלות שאילתה |
| למי זה נוח במיוחד? | אינטגרציות יציבות ומשאבים ברורים | לקוחות רבים עם צרכים משתנים |
מה להכין לפני שבוחרים
לפני שמחליטים, כדאי לאסוף מידע שאינו קשור לכלי מסוים. הבחירה תהיה טובה יותר אם תתחילו ממיפוי התנהגות, לא מטכנולוגיה.
- רשימת לקוחות API — מובייל, דפדפן, מערכות פנימיות, שותפים חיצוניים, סקריפטים תפעוליים.
- מסכים ותהליכים מרכזיים — לא רק ישויות. לדוגמה: “עמוד מוצר”, “סיכום הזמנה”, “תיק לקוח”.
- גבולות בעלות — איזה צוות אחראי על מחיר, מלאי, משתמש, תשלום או הרשאה.
- דרישות שינוי — האם שדות משתנים כל שבוע? האם יש גרסאות מובייל ישנות שחייבות להמשיך לעבוד?
- דרישות אבטחה — הרשאה ברמת משאב בלבד, או גם ברמת שדה?
- תבניות קריאה — האם רוב הקריאות הן CRUD פשוט, או הרכבות עשירות של נתונים?
אפשר לתאר את המיפוי ב-UML פשוט. לא צריך להפוך את זה למסמך כבד. uml class diagram קטן שמראה ישויות וקשרים, ולצידו תרשים רצף שמתאר מסך מרכזי אחד, יעזרו לזהות אם הבעיה היא משאבים יציבים או הרכבת נתונים גמישה. uml diagram טוב כאן הוא כלי חשיבה, לא קישוט.
תהליך בחירה מעשי, שלב אחר שלב
שלב 1: לזהות אם ה-API מייצג משאבים או חוויות שימוש
אם רוב הצרכים הם “תן לי הזמנה”, “עדכן כתובת”, “מחק מסמך”, REST בדרך כלל מתאים. אם רוב הצרכים הם “תן לי למסך הזה בדיוק את השדות האלו, ממספר מקורות”, GraphQL מתחיל להיות מעניין.
שלב 2: לבדוק כמה לקוחות שונים באמת קיימים
GraphQL מצדיק את עצמו יותר כאשר יש שונות אמיתית בין לקוחות. אם יש רק frontend אחד בשליטה מלאה של אותו צוות, REST מותאם היטב עשוי להיות פשוט וזול יותר. לעומת זאת, כאשר אפליקציית מובייל צריכה payload קטן, מערכת ניהול צריכה הרבה שדות, ודוחות פנימיים צריכים חתכים אחרים — סכמה גמישה יכולה להפחית חיכוך.
שלב 3: להחליט איפה מתבצעת הרכבת הנתונים
ב-REST, הרכבה יכולה לקרות בצד הלקוח, בשכבת Backend for Frontend, או ב-endpoint ייעודי. ב-GraphQL, ההרכבה נוטה לעבור לשרת ה-GraphQL ול-resolver-ים. זו לא רק החלטה טכנית; זו החלטה על בעלות, עומסים, בדיקות ותקלות.
שלב 4: להגדיר מדיניות שינוי
ב-REST נפוץ לעבוד עם גרסאות URL או headers. ב-GraphQL נהוג להימנע משבירת סכמה, לסמן שדות כמיושנים, ולהסיר אותם רק אחרי תהליך מסודר. בכל מקרה, שינוי API בלי מדיניות הוא חוב תכנוני.
שלב 5: לבנות ניסוי קטן סביב מקרה אמיתי
אל תבחרו לפי דוגמת “hello world”. קחו מסך אמיתי עם הרשאות, נתונים חסרים, שדות אופציונליים ושני מקורות מידע. אם GraphQL נראה נהדר רק בדוגמה נקייה, אבל מסתבך בהרשאות ובניטור — זו אינדיקציה חשובה. אם REST דורש חמש קריאות וגורם לקוד לקוח שביר — גם זו אינדיקציה.
דוגמת Java: להסתיר את הבחירה מאחורי ממשק תכנוני
הטעות הנפוצה היא לתת ל-REST או ל-GraphQL להתפשט בכל הקוד. מחלקות עסקיות מתחילות להכיר URLs, query strings או מבני response. במקום זאת, אפשר להגדיר ממשק שמתאר צורך עסקי, ולממש אותו פעם אחת מעל REST ופעם אחת מעל GraphQL. הרעיון קרוב לתבניות כמו Adapter ו-Facade. למבוא רחב יותר להקשר של תבניות כאלה, ראו מבוא לתבניות עיצוב.
import java.util.Objects;
public class ApiDesignExample {
public static void main(String[] args) {
ProductQueryApi restApi = new RestCatalogFacade(
new InventoryApi(),
new PricingApi(),
new ReviewsApi()
);
ProductQueryApi graphQlApi = new GraphQlCatalogAdapter(new CatalogGraph());
ProductPagePresenter restPresenter = new ProductPagePresenter(restApi);
ProductPagePresenter graphPresenter = new ProductPagePresenter(graphQlApi);
System.out.println(restPresenter.render("p-100"));
System.out.println(graphPresenter.render("p-100"));
}
}
interface ProductQueryApi {
ProductView productPage(String productId);
}
class ProductPagePresenter {
private final ProductQueryApi api;
ProductPagePresenter(ProductQueryApi api) {
this.api = Objects.requireNonNull(api);
}
String render(String productId) {
return api.productPage(productId).summary();
}
}
class ProductView {
private final String id;
private final String name;
private final int priceInCents;
private final double rating;
ProductView(String id, String name, int priceInCents, double rating) {
this.id = id;
this.name = name;
this.priceInCents = priceInCents;
this.rating = rating;
}
String summary() {
return id + " | " + name + " | " + priceInCents + " cents | rating " + rating;
}
}
class RestCatalogFacade implements ProductQueryApi {
private final InventoryApi inventoryApi;
private final PricingApi pricingApi;
private final ReviewsApi reviewsApi;
RestCatalogFacade(InventoryApi inventoryApi, PricingApi pricingApi, ReviewsApi reviewsApi) {
this.inventoryApi = inventoryApi;
this.pricingApi = pricingApi;
this.reviewsApi = reviewsApi;
}
public ProductView productPage(String productId) {
Product product = inventoryApi.product(productId);
Price price = pricingApi.price(productId);
double rating = reviewsApi.averageRating(productId);
return new ProductView(product.id, product.name, price.cents, rating);
}
}
class GraphQlCatalogAdapter implements ProductQueryApi {
private final CatalogGraph graph;
GraphQlCatalogAdapter(CatalogGraph graph) {
this.graph = graph;
}
public ProductView productPage(String productId) {
String query = "query ProductPage($id: ID!) { product(id: $id) { id name price rating } }";
GraphResult result = graph.execute(query, productId);
return new ProductView(result.id, result.name, result.priceInCents, result.rating);
}
}
class InventoryApi {
Product product(String id) {
return new Product(id, "Mechanical Keyboard");
}
}
class PricingApi {
Price price(String productId) {
return new Price(12990);
}
}
class ReviewsApi {
double averageRating(String productId) {
return 4.7;
}
}
class CatalogGraph {
GraphResult execute(String query, String id) {
return new GraphResult(id, "Mechanical Keyboard", 12990, 4.7);
}
}
class Product {
final String id;
final String name;
Product(String id, String name) {
this.id = id;
this.name = name;
}
}
class Price {
final int cents;
Price(int cents) {
this.cents = cents;
}
}
class GraphResult {
final String id;
final String name;
final int priceInCents;
final double rating;
GraphResult(String id, String name, int priceInCents, double rating) {
this.id = id;
this.name = name;
this.priceInCents = priceInCents;
this.rating = rating;
}
}בדוגמה, ProductQueryApi הוא החוזה שהקוד הפנימי צריך. RestCatalogFacade מסתיר קריאות REST מרובות מאחורי פעולה אחת שמתאימה למסך. GraphQlCatalogAdapter מסתיר שאילתת GraphQL ומחזיר אותו מודל פנימי. ProductPagePresenter לא יודע ולא צריך לדעת באיזה סגנון API השתמשו. זה יישום פשוט של עקרון הפרדת אחריות ושל תלות בהפשטה, לא בפרוטוקול.
| רכיב | תפקיד תכנוני |
|---|---|
ProductQueryApi |
ממשק שמייצג צורך עסקי: נתוני עמוד מוצר |
RestCatalogFacade |
מאחד כמה משאבי REST לפעולה אחת |
GraphQlCatalogAdapter |
מתאים שאילתת GraphQL לאותו חוזה פנימי |
ProductPagePresenter |
משתמש בהפשטה ואינו תלוי בצורת ה-API |
GraphQL ו-REST בתוך microservices architecture
במערכת מיקרו-שירותים, השאלה מסתבכת. REST משתלב באופן טבעי עם שירותים שמפרסמים משאבים. שירות מלאי מפרסם מלאי, שירות מחירים מפרסם מחירים, שירות ביקורות מפרסם ביקורות. זה ברור, אבל הלקוח עלול להפוך למתאם של כל השירותים.
GraphQL יכול לשמש שכבת איחוד מעל שירותים קיימים. הלקוח רואה גרף אחד, בעוד שמאחוריו יש כמה שירותים. זה נוח, אבל מסוכן אם שכבת GraphQL הופכת ל”שירות על” שמכיר את כל המערכת, עוקף גבולות צוותים ומרכז בתוכו לוגיקה עסקית שלא שייכת אליו.
כלל אצבע שימושי: GraphQL טוב כשכבת קומפוזיציה, לא כמקום להחביא מודל דומיין מבולגן. אם אין גבולות ברורים בין שירותים, GraphQL לא יתקן את זה; הוא רק ייתן לבלגן ממשק אלגנטי יותר.
טעויות נפוצות ומה עושים כשהן מופיעות
טעות: endpoint REST אחד לכל מסך
זה מתחיל תמים: /home-page, /product-page, /checkout-summary. לפעמים זה נכון, בעיקר ב-Backend for Frontend. אבל אם כל שינוי קטן במסך יוצר endpoint חדש, מאבדים את היתרון של REST כמודל משאבים. במקרה כזה כדאי לבדוק אם שכבת קומפוזיציה, אולי GraphQL, מתאימה יותר.
טעות: סכמה GraphQL ללא גבולות בעלות
כאשר כל צוות מוסיף שדות לאותה סכמה בלי כללים, נוצר חוזה ענק שאיש לא מבין. הפתרון הוא להגדיר בעלות על סוגים ושדות, תהליך סימון deprecation, ובדיקות תאימות. סכמה היא מוצר תכנוני, לא תוצר לוואי של קוד.
טעות: התעלמות מבעיית N+1
GraphQL עלול לגרום לכך ששדה אחד מפעיל קריאה נוספת לכל פריט ברשימה. במסך עם מאה מוצרים זו כבר תקלה ביצועית. צריך למדוד resolver-ים, להשתמש באיסוף קריאות ובטעינה מרוכזת, ולהגדיר מגבלות עומק ועלות לשאילתות.
טעות: בחירה בלי מודל הרשאות
ב-REST הרשאה לפי endpoint ומשאב ברורה יחסית. ב-GraphQL צריך לחשוב גם על הרשאות שדה: ייתכן שמשתמש רשאי לראות שם מוצר אך לא מחיר ספק. אם אין פתרון מסודר לזה, GraphQL עלול לחשוף מידע בטעות דרך שילוב שדות שלא נבדק.
הקשר לתבניות עיצוב ו-SOLID
בחירת API אינה מנותקת מתכנון מונחה עצמים. עקרון Dependency Inversion מ-SOLID אומר שקוד מדיניות לא צריך להיות תלוי בפרטים נמוכים. בדוגמה, המסך תלוי ב-ProductQueryApi, לא ב-URL ולא בשאילתת GraphQL. כך אפשר להחליף מימוש, לבדוק אותו, או לפצל אותו בלי לשכתב את כל המערכת.
גם תבניות יצירה יכולות להיכנס לתמונה כאשר צריך לבנות לקוחות API בהתאם לסביבה, גרסה או tenant. למשל, Factory Method או Abstract Factory עשויות להתאים כאשר יש משפחות של לקוחות שירותים. הרחבה על הכיוון הזה נמצאת בעמוד תבניות יצירה, אבל חשוב לזכור: תבנית אינה תחליף להחלטה אדריכלית. היא רק דרך לממש אותה בצורה נקייה.
מתי לא להשתמש ב-GraphQL
GraphQL אינו בחירה טובה כאשר הצרכים פשוטים, המשאבים יציבים, מספר הלקוחות קטן, והצוות אינו ערוך לנהל סכמה, ניטור והרשאות ברמת שדה. אם כל מה שצריך הוא CRUD ברור למערכת פנימית אחת, REST יהיה לעיתים קרובות פשוט יותר, זול יותר לתפעול וקל יותר לאבחון.
כדאי להיזהר במיוחד אם בוחרים GraphQL רק כדי “לחסוך endpoints”. מספר endpoints אינו בעיה בפני עצמה. הבעיה היא חוזים לא יציבים, כפילות, תלויות לא ברורות וחוסר מדידה.
מתי לא להישאר עם REST בלבד
REST מתחיל לחרוק כאשר כל מסך דורש orchestration ידני בצד הלקוח, כאשר לקוחות שונים צריכים חתכים שונים מאוד של אותו עולם נתונים, או כאשר שינוי קטן בצרכי מובייל גורר שינוי חוזר בשרת. במצב כזה אפשר לשקול GraphQL, או שכבת BFF ייעודית, או שילוב של שניהם.
גם כאן אין חובה לבחור צד אחד לנצח. מערכות רבות מחזיקות REST בין שירותים פנימיים ו-GraphQL מול לקוחות מסוימים. העיקר הוא לא לערבב אחריות: REST פנימי למשאבים יציבים, GraphQL חיצוני לקומפוזיציה, וחוזים ברורים ביניהם.
סיכום מעשי
REST מצטיין כאשר רוצים מודל משאבים פשוט, יציב, מדיד ומובן. GraphQL מצטיין כאשר יש לקוחות רבים, צרכי נתונים משתנים והרכבות מורכבות של מידע. REST עולה לפעמים בריבוי קריאות או endpoints מותאמים מדי; GraphQL עולה במורכבות שרת, הרשאות, ניטור ובקרת ביצועים.
הבחירה הטובה מתחילה ממיפוי צרכים: מי הלקוחות, אילו מסכים קיימים, איפה גבולות השירותים, מה משתנה, ומה חייב להיות בטוח ומדיד. אם שומרים את הפרוטוקול מאחורי הפשטה נקייה, קל יותר לשנות כיוון בהמשך. API הוא חוזה; GraphQL ו-REST הם רק שתי דרכים שונות לעצב את החוזה הזה.


