דלג לתוכן הראשי
שכבת הפשטה ארכיטקטורת תוכנה בעברית
תכנון API

אידמפוטנטיות ב-API: איך מונעים חיוב כפול מבקשה כפולה

בקשת API שנשלחת פעמיים היא לא תקלה נדירה. משתמש לחץ שוב על כפתור התשלום, הדפדפן ניסה לשלוח מחדש, לקוח מובייל איבד קליטה, או שירות פנימי קיבל timeout והחליט לבצע retry. אם הפעולה היא קריאה תמימה, הנזק קטן. אם הפעולה מחייבת כרטיס, יוצרת הזמנה, שולחת הודעה או משנה מלאי — בקשה כפולה עלולה להפוך לבאג יקר.

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

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

מהי אידמפוטנטיות, ומה היא לא

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

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

חשוב להפריד בין שלושה מושגים שמתבלבלים ביניהם:

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

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

למה בקשות כפולות קורות גם במערכות “תקינות”

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

  • לקוח קיבל timeout, אבל השרת כבר ביצע את הפעולה.
  • Load balancer ניתק חיבור באמצע תשובה.
  • אפליקציית מובייל עברה בין רשתות ושלחה מחדש.
  • Message broker העביר הודעה שוב כי לא התקבל אישור עיבוד.
  • שירות פנימי משתמש במדיניות retry אוטומטית.
  • משתמש רענן עמוד אחרי שליחת טופס.

בארכיטקטורת microservices, הבעיה חריפה יותר. שירות אחד עשוי לקרוא לשירות שני, שמדבר עם שירות שלישי, וכל אחד מהם עלול לבצע retry משלו. אם אין חוזה ברור סביב אידמפוטנטיות, קל לקבל שרשרת שבה “רק ניסינו שוב” הופך לחיוב כפול, הזמנה כפולה או הקטנת מלאי כפולה.

העיקרון המרכזי: מפתח אידמפוטנטיות

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

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

מפתח אידמפוטנטיות טוב צריך להיות:

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

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

תרחיש לדוגמה: יצירת תשלום

נניח שיש endpoint בשם POST /payments. הלקוח שולח מזהה הזמנה, סכום, מטבע ומפתח אידמפוטנטיות. השרת צריך לבצע חיוב מול ספק תשלומים, לשמור רשומה, ולהחזיר קבלה.

זרימה בסיסית יכולה להיראות כך:

  1. השרת מקבל בקשה עם Idempotency-Key.
  2. השרת מחשב fingerprint לתוכן הבקשה.
  3. השרת מחפש רשומה קיימת עבור המפתח.
  4. אם נמצאה רשומה שהושלמה ותוכן הבקשה תואם — מחזירים את התגובה השמורה.
  5. אם נמצאה רשומה בעיבוד — מחזירים סטטוס מתאים, או ממתינים בהתאם למדיניות.
  6. אם לא נמצאה רשומה — יוצרים רשומת “בעיבוד” בצורה אטומית.
  7. מבצעים את הפעולה המסוכנת.
  8. שומרים את התוצאה הסופית ומחזירים אותה.

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

מה צריך לשמור בשרת

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

שדה תפקיד הערה תכנונית
idempotency_key מזהה הפעולה הלוגית צריך להיות ייחודי בהקשר מתאים, למשל לפי לקוח או חשבון
request_fingerprint זיהוי תוכן הבקשה מונע שימוש חוזר במפתח עבור בקשה אחרת
status מצב העיבוד למשל PROCESSING, COMPLETED, FAILED
response_body התוצאה שהוחזרה ללקוח מאפשרת replay של אותה תשובה
created_at זמן יצירת הרשומה משמש לניקוי רשומות ישנות

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

דוגמת Java מלאה: שירות תשלומים אידמפוטנטי

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

Java
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        IdempotencyStore store = new InMemoryIdempotencyStore();
        PaymentGateway gateway = new FakePaymentGateway();
        PaymentService service = new PaymentService(store, gateway);

        PaymentRequest request = new PaymentRequest("order-123", 1200, "ILS");
        String key = "client-generated-key-abc";

        PaymentReceipt first = service.pay(key, request);
        PaymentReceipt second = service.pay(key, request);

        System.out.println(first);
        System.out.println(second);
        System.out.println("Same receipt id: " + first.receiptId.equals(second.receiptId));
    }

    static final class PaymentRequest {
        final String orderId;
        final int amountInMinorUnits;
        final String currency;

        PaymentRequest(String orderId, int amountInMinorUnits, String currency) {
            this.orderId = orderId;
            this.amountInMinorUnits = amountInMinorUnits;
            this.currency = currency;
        }

        String fingerprint() {
            return orderId + "|" + amountInMinorUnits + "|" + currency;
        }
    }

    static final class PaymentReceipt {
        final String receiptId;
        final String orderId;
        final int amountInMinorUnits;
        final String currency;

        PaymentReceipt(String receiptId, String orderId, int amountInMinorUnits, String currency) {
            this.receiptId = receiptId;
            this.orderId = orderId;
            this.amountInMinorUnits = amountInMinorUnits;
            this.currency = currency;
        }

        @Override
        public String toString() {
            return "PaymentReceipt{" +
                    "receiptId='" + receiptId + '\'' +
                    ", orderId='" + orderId + '\'' +
                    ", amountInMinorUnits=" + amountInMinorUnits +
                    ", currency='" + currency + '\'' +
                    '}';
        }
    }

    enum Status {
        PROCESSING,
        COMPLETED
    }

    static final class IdempotencyRecord {
        final String fingerprint;
        Status status;
        PaymentReceipt receipt;

        IdempotencyRecord(String fingerprint) {
            this.fingerprint = fingerprint;
            this.status = Status.PROCESSING;
        }
    }

    interface IdempotencyStore {
        IdempotencyRecord getOrCreate(String key, String fingerprint);
        void complete(String key, PaymentReceipt receipt);
    }

    static final class InMemoryIdempotencyStore implements IdempotencyStore {
        private final Map<String, IdempotencyRecord> records = new HashMap<>();

        @Override
        public synchronized IdempotencyRecord getOrCreate(String key, String fingerprint) {
            IdempotencyRecord existing = records.get(key);
            if (existing != null) {
                if (!Objects.equals(existing.fingerprint, fingerprint)) {
                    throw new IllegalArgumentException("Idempotency key reused with a different request");
                }
                return existing;
            }

            IdempotencyRecord created = new IdempotencyRecord(fingerprint);
            records.put(key, created);
            return created;
        }

        @Override
        public synchronized void complete(String key, PaymentReceipt receipt) {
            IdempotencyRecord record = records.get(key);
            if (record == null) {
                throw new IllegalStateException("Missing idempotency record");
            }
            record.receipt = receipt;
            record.status = Status.COMPLETED;
        }
    }

    interface PaymentGateway {
        PaymentReceipt charge(PaymentRequest request);
    }

    static final class FakePaymentGateway implements PaymentGateway {
        private int sequence = 1;

        @Override
        public PaymentReceipt charge(PaymentRequest request) {
            String receiptId = "receipt-" + sequence++;
            return new PaymentReceipt(
                    receiptId,
                    request.orderId,
                    request.amountInMinorUnits,
                    request.currency
            );
        }
    }

    static final class PaymentService {
        private final IdempotencyStore store;
        private final PaymentGateway gateway;

        PaymentService(IdempotencyStore store, PaymentGateway gateway) {
            this.store = store;
            this.gateway = gateway;
        }

        PaymentReceipt pay(String idempotencyKey, PaymentRequest request) {
            IdempotencyRecord record = store.getOrCreate(idempotencyKey, request.fingerprint());

            if (record.status == Status.COMPLETED) {
                return record.receipt;
            }

            PaymentReceipt receipt = gateway.charge(request);
            store.complete(idempotencyKey, receipt);
            return receipt;
        }
    }
}

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

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

התמודדות עם בקשות במצב ביניים

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

יש כמה מדיניות אפשריות:

  • להחזיר “עדיין בעיבוד” — למשל תשובה שמבהירה ללקוח לנסות שוב מאוחר יותר.
  • להמתין זמן קצר — מתאים כשזמן העיבוד צפוי להיות קצר ורוצים להחזיר תוצאה סופית.
  • להעביר לעיבוד אסינכרוני — הלקוח מקבל מזהה פעולה ובודק סטטוס בהמשך.
  • לבצע reconciliation — לבדוק מול מערכת חיצונית אם הפעולה כבר בוצעה.

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

אידמפוטנטיות בבסיס הנתונים

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

נקודות שחשוב לתכנן מראש:

  • ייחודיות — הגדרת constraint שמונע שתי רשומות לאותו מפתח.
  • טרנזקציה — יצירת רשומת “בעיבוד” והחלטה מי מבצע את הפעולה.
  • נעילות — מניעת מצב שבו שתי בקשות קוראות “אין רשומה” ואז שתיהן מחייבות.
  • TTL או ניקוי — רשומות אידמפוטנטיות לא חייבות להישמר לנצח, אבל צריך להגדיר חלון זמן.
  • Audit — בפעולות כספיות או רגישות, רצוי להשאיר עקבות שמאפשרים להבין מה קרה.

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

מה עושים כשאותו מפתח מגיע עם גוף אחר

זה אחד המקומות שבהם מערכות נוטות להיות “נחמדות” מדי. לקוח שולח אותו Idempotency-Key עם סכום אחד, ואז שולח שוב עם סכום אחר. האם להחזיר את התשלום הראשון? האם ליצור תשלום חדש? האם להתעלם מההבדל?

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

צריך לשים לב אילו שדות נכנסים ל-fingerprint. שדות טכניים כמו timestamp של שליחה עלולים לשבור retry תקין. לעומת זאת, סכום, מטבע, מזהה הזמנה וזהות הלקוח הם לרוב חלק מהותי מהבקשה.

קשר ל-microservices ול-API design

בארכיטקטורת microservices, אידמפוטנטיות אינה רק עניין של endpoint ציבורי. כל שירות שמקבל פקודה משירות אחר צריך לשאול: “אם אותה פקודה תגיע שוב, מה יקרה?”. במיוחד בתקשורת אסינכרונית, מודל נפוץ הוא at-least-once delivery: ההודעה תגיע לפחות פעם אחת, אבל אולי יותר.

לכן כדאי להגדיר בכל API:

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

אפשר לראות בזה חלק מחוזה ה-API לא פחות משמות השדות. endpoint שמחזיר JSON יפה אבל לא מגדיר התנהגות retry משאיר את הלקוח לנחש. במערכות אמיתיות, ניחוש כזה הופך מהר מאוד לקוד הגנתי מפוזר, שכפול לוגיקה ותקשורת לא אמינה בין צוותים.

איפה נכנסות תבניות עיצוב

אידמפוטנטיות אינה אחת מתבניות GoF הקלאסיות מתוך Design Patterns, Gamma et al., 1994, אבל דרך היישום שלה נהנית מאותם עקרונות: הפרדת אחריות, הסתרת פרטי מימוש, ותכנון נקודת הרחבה ברורה. למשל, אפשר לעטוף פעולה מסוכנת באובייקט שמוסיף מדיניות אידמפוטנטיות לפני הקריאה בפועל, בדומה לרעיון הכללי של שכבת ביניים סביב רכיב.

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

טעויות נפוצות בתכנון אידמפוטנטיות

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

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

מתי לא להשתמש במנגנון אידמפוטנטיות מלא

לא כל פעולה צריכה Idempotency-Key. אם מדובר בקריאת מידע, אין צורך. אם מדובר בעדכון מצב מוחלט, כמו “קבע את כתובת המשלוח לערך הזה”, ייתכן ש-PUT עם מזהה משאב ברור מספיק. אם הפעולה זולה וניתנת לתיקון בקלות, מנגנון מלא עשוי להיות מוגזם.

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

בדיקות שכדאי לכתוב

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

  • שתי בקשות זהות עם אותו מפתח מחזירות אותה תוצאה.
  • אותו מפתח עם גוף שונה מחזיר שגיאה.
  • שתי בקשות מקבילות אינן מבצעות את הפעולה פעמיים.
  • בקשה חוזרת אחרי timeout מקבלת את התוצאה שנשמרה.
  • רשומות ישנות נמחקות רק אחרי חלון זמן שמתאים לחוזה ה-API.
  • כשל בשירות חיצוני לא משאיר מצב שאי אפשר להתאושש ממנו.

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

סיכום מעשי

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

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

נכתב על ידי

ליאת שגיא : כותב

כותבת על SOLID, תכנון מונחה עצמים, UML, מיקרו־שירותים ותכנון API. לא ארכיטקטית מוסמכת ולא נציגה של חברה או קורס — מסבירה החלטות תכנון עם יתרונות, חסרונות ודוגמה לפני ואחרי.

להמשך קריאה

לכל המאמרים בנושא

תכנון API

ניהול גרסאות API בלי כאב ראש: URL, כותרת או בלי גרסאות — ואיך שומרים על התאמה לאחור

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

10 דק׳ קריאה