[{"data":1,"prerenderedAt":1889},["ShallowReactive",2],{"tag-versioning":3},[4],{"_path":5,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":9,"description":10,"layout":11,"date":12,"subtitle":13,"image":14,"optimized_image":14,"category":15,"tags":16,"author":21,"paginate":7,"body":22,"_type":1883,"_id":1884,"_source":1885,"_file":1886,"_stem":1887,"_extension":1888},"/posts/api-evolution-strategy","posts",false,"","Your API Needs an Evolution Strategy","A practical guide to evolving public APIs: compatibility, version selection, client migration, deprecation, contract testing, and safe rollback.","post","2025-05-15T11:00:00.000Z","Design changes around your clients and their ability to deploy independently","/assets/img/uploads/api-evolution-strategy.jpg","code",[15,17,18,19,20],"api-first","openapi","architecture","versioning","jaimedearcos",{"type":23,"children":24,"toc":1872},"root",[25,33,38,49,54,61,66,87,103,108,114,119,124,247,260,272,277,283,288,302,307,400,422,435,440,625,661,667,680,793,798,883,888,1027,1040,1077,1082,1087,1093,1098,1133,1138,1226,1238,1243,1248,1253,1259,1264,1301,1306,1354,1407,1412,1417,1423,1428,1433,1520,1525,1758,1770,1784,1790,1795,1800,1805,1811,1866],{"type":26,"tag":27,"props":28,"children":29},"element","p",{},[30],{"type":31,"value":32},"text","In my experience, teams used to building internal applications sometimes bring an assumption into API design: if an interface changes, we can coordinate with the people consuming it and deploy together. I have seen that assumption create substantial extra work around release planning and rollback. When several deployments depend on each other, a failure in one can leave the system in a combination nobody intended to run. Users can lose access to a service while teams work out how to recover it.",{"type":26,"tag":27,"props":34,"children":35},{},[36],{"type":31,"value":37},"I understand how that habit develops. When the consumer is another application in the same organization, its developers may be one conversation away. A coordinated release can feel like a reasonable shortcut. But public API clients have their own priorities, approval processes, maintenance windows, and users. Their release schedule does not belong to us.",{"type":26,"tag":27,"props":39,"children":40},{},[41,47],{"type":26,"tag":42,"props":43,"children":44},"strong",{},[45],{"type":31,"value":46},"If an API change requires your clients to deploy at the same time as you, treat that as a design warning.",{"type":31,"value":48}," Before accepting the coordination cost, ask whether the contract could support a period in which both old and new clients work.",{"type":26,"tag":27,"props":50,"children":51},{},[52],{"type":31,"value":53},"Putting yourself in the client's place changes the design discussion. Can they keep serving their users while they migrate? Can they test the new behavior before switching? Can either side recover from a failed release independently? Those questions turn versioning into an engineering strategy.",{"type":26,"tag":55,"props":56,"children":58},"h2",{"id":57},"start-with-the-contract-a-client-already-depends-on",[59],{"type":31,"value":60},"Start with the contract a client already depends on",{"type":26,"tag":27,"props":62,"children":63},{},[64],{"type":31,"value":65},"An API contract includes more than paths and JSON fields. Clients depend on validation rules, status codes, authorization requirements, default ordering, pagination, error responses, and the meaning of the values you return.",{"type":26,"tag":27,"props":67,"children":68},{},[69,71,77,79,85],{"type":31,"value":70},"A response can remain valid JSON while breaking an integration. Changing an amount from euros to cents preserves its numeric type but changes what it means. Replacing a synchronous ",{"type":26,"tag":15,"props":72,"children":74},{"className":73},[],[75],{"type":31,"value":76},"201 Created",{"type":31,"value":78}," workflow with ",{"type":26,"tag":15,"props":80,"children":82},{"className":81},[],[83],{"type":31,"value":84},"202 Accepted",{"type":31,"value":86}," asks the client to handle a different lifecycle. Making a search return only the first page when it previously returned all matches can silently lose data from an export.",{"type":26,"tag":27,"props":88,"children":89},{},[90,92,101],{"type":31,"value":91},"Compatibility therefore needs several kinds of review: can existing code communicate with the service, interpret the result correctly, and still perform its intended workflow? Google's ",{"type":26,"tag":93,"props":94,"children":98},"a",{"href":95,"rel":96},"https://google.aip.dev/180",[97],"nofollow",[99],{"type":31,"value":100},"backwards compatibility guidance",{"type":31,"value":102}," distinguishes wire, source, and semantic compatibility; it is a useful framework, even when your API follows a different style.",{"type":26,"tag":27,"props":104,"children":105},{},[106],{"type":31,"value":107},"Before designing a change, take one existing client journey and write it down. For an order search, that could be: authenticate, request a page, deserialize the response, display the orders, request the next page, and recover from an error. Review the change against that sequence, including the code the customer deployed months ago.",{"type":26,"tag":55,"props":109,"children":111},{"id":110},"compatibility-has-a-direction",[112],{"type":31,"value":113},"Compatibility has a direction",{"type":26,"tag":27,"props":115,"children":116},{},[117],{"type":31,"value":118},"For an existing API version, the provider normally needs to keep accepting previously valid requests and returning responses that existing clients can handle. That does not automatically mean a new client can use new features against an old server. This distinction matters during rolling deployments and rollback.",{"type":26,"tag":27,"props":120,"children":121},{},[122],{"type":31,"value":123},"Consider these changes:",{"type":26,"tag":125,"props":126,"children":127},"table",{},[128,152],{"type":26,"tag":129,"props":130,"children":131},"thead",{},[132],{"type":26,"tag":133,"props":134,"children":135},"tr",{},[136,142,147],{"type":26,"tag":137,"props":138,"children":139},"th",{},[140],{"type":31,"value":141},"Proposed change",{"type":26,"tag":137,"props":143,"children":144},{},[145],{"type":31,"value":146},"Effect on an existing client",{"type":26,"tag":137,"props":148,"children":149},{},[150],{"type":31,"value":151},"Safer approach",{"type":26,"tag":153,"props":154,"children":155},"tbody",{},[156,175,193,211,229],{"type":26,"tag":133,"props":157,"children":158},{},[159,165,170],{"type":26,"tag":160,"props":161,"children":162},"td",{},[163],{"type":31,"value":164},"Require a new request field",{"type":26,"tag":160,"props":166,"children":167},{},[168],{"type":31,"value":169},"Requests that used to work can fail validation",{"type":26,"tag":160,"props":171,"children":172},{},[173],{"type":31,"value":174},"Make it optional with a documented default that preserves the old behavior, or introduce a new contract",{"type":26,"tag":133,"props":176,"children":177},{},[178,183,188],{"type":26,"tag":160,"props":179,"children":180},{},[181],{"type":31,"value":182},"Remove or rename a response field",{"type":26,"tag":160,"props":184,"children":185},{},[186],{"type":31,"value":187},"Deserialization or application logic can fail",{"type":26,"tag":160,"props":189,"children":190},{},[191],{"type":31,"value":192},"Retain the field during support, or offer the new representation in a separately selected version",{"type":26,"tag":133,"props":194,"children":195},{},[196,201,206],{"type":26,"tag":160,"props":197,"children":198},{},[199],{"type":31,"value":200},"Add a response field",{"type":26,"tag":160,"props":202,"children":203},{},[204],{"type":31,"value":205},"Often compatible, but strict decoders or schemas can reject it",{"type":26,"tag":160,"props":207,"children":208},{},[209],{"type":31,"value":210},"Establish and test a policy for unknown response fields",{"type":26,"tag":133,"props":212,"children":213},{},[214,219,224],{"type":26,"tag":160,"props":215,"children":216},{},[217],{"type":31,"value":218},"Return a new enum value",{"type":26,"tag":160,"props":220,"children":221},{},[222],{"type":31,"value":223},"A generated enum parser or exhaustive branch can fail",{"type":26,"tag":160,"props":225,"children":226},{},[227],{"type":31,"value":228},"Define extensible-enum behavior up front; otherwise gate the value behind an opt-in contract",{"type":26,"tag":133,"props":230,"children":231},{},[232,237,242],{"type":26,"tag":160,"props":233,"children":234},{},[235],{"type":31,"value":236},"Change ordering, units, or error behavior",{"type":26,"tag":160,"props":238,"children":239},{},[240],{"type":31,"value":241},"A client may parse the response and still behave incorrectly",{"type":26,"tag":160,"props":243,"children":244},{},[245],{"type":31,"value":246},"Treat the behavioral change as part of compatibility review",{"type":26,"tag":27,"props":248,"children":249},{},[250,252,258],{"type":31,"value":251},"For example, adding an optional ",{"type":26,"tag":15,"props":253,"children":255},{"className":254},[],[256],{"type":31,"value":257},"deliveryInstructions",{"type":31,"value":259}," field to order creation can preserve old clients: omission must continue to produce the previous delivery behavior. The new server must support that omission deliberately. Requiring clients to start sending an empty string would still force a migration.",{"type":26,"tag":27,"props":261,"children":262},{},[263,265,270],{"type":31,"value":264},"Conversely, a new client that sends ",{"type":26,"tag":15,"props":266,"children":268},{"className":267},[],[269],{"type":31,"value":257},{"type":31,"value":271}," cannot assume an older server will accept or honor it. Deploy support before enabling its use, and decide what happens if the provider rolls back.",{"type":26,"tag":27,"props":273,"children":274},{},[275],{"type":31,"value":276},"Do not assume that every client ignores unknown response fields. If you publish an SDK, exercise its actual decoder. If your documented response schema prohibits additional properties, adding one can violate the contract you supplied. Define extension rules early and apply them consistently; changing the rules after clients have shipped does not update those clients.",{"type":26,"tag":55,"props":278,"children":280},{"id":279},"separate-the-api-version-from-your-deployment-version",[281],{"type":31,"value":282},"Separate the API version from your deployment version",{"type":26,"tag":27,"props":284,"children":285},{},[286],{"type":31,"value":287},"A backend can have many releases while continuing to serve the same public contract. A database migration, a refactor, and a performance improvement do not each need a new client-facing API version.",{"type":26,"tag":27,"props":289,"children":290},{},[291,293,300],{"type":31,"value":292},"Use a new major contract when you need an incompatible change that cannot reasonably be delivered within the existing promises. Compatible additions can remain in the supported version. Google's ",{"type":26,"tag":93,"props":294,"children":297},{"href":295,"rel":296},"https://google.aip.dev/185",[97],[298],{"type":31,"value":299},"API versioning guidance",{"type":31,"value":301}," describes this separation between a service's evolution and the versions consumers select.",{"type":26,"tag":27,"props":303,"children":304},{},[305],{"type":31,"value":306},"The selection mechanism should be explicit and predictable:",{"type":26,"tag":125,"props":308,"children":309},{},[310,331],{"type":26,"tag":129,"props":311,"children":312},{},[313],{"type":26,"tag":133,"props":314,"children":315},{},[316,321,326],{"type":26,"tag":137,"props":317,"children":318},{},[319],{"type":31,"value":320},"Mechanism",{"type":26,"tag":137,"props":322,"children":323},{},[324],{"type":31,"value":325},"Example",{"type":26,"tag":137,"props":327,"children":328},{},[329],{"type":31,"value":330},"Operational consideration",{"type":26,"tag":153,"props":332,"children":333},{},[334,356,378],{"type":26,"tag":133,"props":335,"children":336},{},[337,342,351],{"type":26,"tag":160,"props":338,"children":339},{},[340],{"type":31,"value":341},"URL path",{"type":26,"tag":160,"props":343,"children":344},{},[345],{"type":26,"tag":15,"props":346,"children":348},{"className":347},[],[349],{"type":31,"value":350},"/v2/orders",{"type":26,"tag":160,"props":352,"children":353},{},[354],{"type":31,"value":355},"Visible in requests, routing, logs, and documentation",{"type":26,"tag":133,"props":357,"children":358},{},[359,364,373],{"type":26,"tag":160,"props":360,"children":361},{},[362],{"type":31,"value":363},"Query parameter",{"type":26,"tag":160,"props":365,"children":366},{},[367],{"type":26,"tag":15,"props":368,"children":370},{"className":369},[],[371],{"type":31,"value":372},"/orders?api-version=2025-05-15",{"type":26,"tag":160,"props":374,"children":375},{},[376],{"type":31,"value":377},"Clients, gateways, and caches must preserve the parameter",{"type":26,"tag":133,"props":379,"children":380},{},[381,386,395],{"type":26,"tag":160,"props":382,"children":383},{},[384],{"type":31,"value":385},"Request header or media type",{"type":26,"tag":160,"props":387,"children":388},{},[389],{"type":26,"tag":15,"props":390,"children":392},{"className":391},[],[393],{"type":31,"value":394},"Accept: application/vnd.example.orders.v2+json",{"type":26,"tag":160,"props":396,"children":397},{},[398],{"type":31,"value":399},"Gateways and representation caches must account for the selecting header",{"type":26,"tag":27,"props":401,"children":402},{},[403,405,411,413,420],{"type":31,"value":404},"I would use paths for the examples here because they make the two contracts easy to see. Other approaches work if routing, documentation, observability, and caching agree on the selection. With header-based representation selection, configure the cache key correctly and use the appropriate ",{"type":26,"tag":15,"props":406,"children":408},{"className":407},[],[409],{"type":31,"value":410},"Vary",{"type":31,"value":412}," response header; ",{"type":26,"tag":93,"props":414,"children":417},{"href":415,"rel":416},"https://www.rfc-editor.org/rfc/rfc9110.html#name-vary",[97],[418],{"type":31,"value":419},"HTTP semantics",{"type":31,"value":421}," explains its role.",{"type":26,"tag":27,"props":423,"children":424},{},[425,427,433],{"type":31,"value":426},"Avoid silently moving an existing client to a newer contract because it omitted a version or requested a moving ",{"type":26,"tag":15,"props":428,"children":430},{"className":429},[],[431],{"type":31,"value":432},"latest",{"type":31,"value":434}," alias. Document the default, keep it stable for existing integrations, and reject unsupported explicit versions clearly.",{"type":26,"tag":27,"props":436,"children":437},{},[438],{"type":31,"value":439},"Also distinguish the version fields in an OpenAPI document:",{"type":26,"tag":441,"props":442,"children":446},"pre",{"className":443,"code":444,"language":445,"meta":8,"style":8},"language-yaml shiki shiki-themes github-dark github-light","openapi: 3.1.0\ninfo:\n  title: Orders API\n  version: 2.0.0\npaths:\n  /v2/orders:\n    get:\n      summary: List orders using cursor pagination\n      responses:\n        '200':\n          description: An envelope containing items and the next cursor\n","yaml",[447],{"type":26,"tag":15,"props":448,"children":449},{"__ignoreMap":8},[450,473,487,506,524,537,550,563,581,594,607],{"type":26,"tag":451,"props":452,"children":455},"span",{"class":453,"line":454},"line",1,[456,461,467],{"type":26,"tag":451,"props":457,"children":459},{"style":458},"--shiki-default:#85E89D;--shiki-light:#22863A",[460],{"type":31,"value":18},{"type":26,"tag":451,"props":462,"children":464},{"style":463},"--shiki-default:#E1E4E8;--shiki-light:#24292E",[465],{"type":31,"value":466},": ",{"type":26,"tag":451,"props":468,"children":470},{"style":469},"--shiki-default:#79B8FF;--shiki-light:#005CC5",[471],{"type":31,"value":472},"3.1.0\n",{"type":26,"tag":451,"props":474,"children":476},{"class":453,"line":475},2,[477,482],{"type":26,"tag":451,"props":478,"children":479},{"style":458},[480],{"type":31,"value":481},"info",{"type":26,"tag":451,"props":483,"children":484},{"style":463},[485],{"type":31,"value":486},":\n",{"type":26,"tag":451,"props":488,"children":490},{"class":453,"line":489},3,[491,496,500],{"type":26,"tag":451,"props":492,"children":493},{"style":458},[494],{"type":31,"value":495},"  title",{"type":26,"tag":451,"props":497,"children":498},{"style":463},[499],{"type":31,"value":466},{"type":26,"tag":451,"props":501,"children":503},{"style":502},"--shiki-default:#9ECBFF;--shiki-light:#032F62",[504],{"type":31,"value":505},"Orders API\n",{"type":26,"tag":451,"props":507,"children":509},{"class":453,"line":508},4,[510,515,519],{"type":26,"tag":451,"props":511,"children":512},{"style":458},[513],{"type":31,"value":514},"  version",{"type":26,"tag":451,"props":516,"children":517},{"style":463},[518],{"type":31,"value":466},{"type":26,"tag":451,"props":520,"children":521},{"style":469},[522],{"type":31,"value":523},"2.0.0\n",{"type":26,"tag":451,"props":525,"children":527},{"class":453,"line":526},5,[528,533],{"type":26,"tag":451,"props":529,"children":530},{"style":458},[531],{"type":31,"value":532},"paths",{"type":26,"tag":451,"props":534,"children":535},{"style":463},[536],{"type":31,"value":486},{"type":26,"tag":451,"props":538,"children":540},{"class":453,"line":539},6,[541,546],{"type":26,"tag":451,"props":542,"children":543},{"style":458},[544],{"type":31,"value":545},"  /v2/orders",{"type":26,"tag":451,"props":547,"children":548},{"style":463},[549],{"type":31,"value":486},{"type":26,"tag":451,"props":551,"children":553},{"class":453,"line":552},7,[554,559],{"type":26,"tag":451,"props":555,"children":556},{"style":458},[557],{"type":31,"value":558},"    get",{"type":26,"tag":451,"props":560,"children":561},{"style":463},[562],{"type":31,"value":486},{"type":26,"tag":451,"props":564,"children":566},{"class":453,"line":565},8,[567,572,576],{"type":26,"tag":451,"props":568,"children":569},{"style":458},[570],{"type":31,"value":571},"      summary",{"type":26,"tag":451,"props":573,"children":574},{"style":463},[575],{"type":31,"value":466},{"type":26,"tag":451,"props":577,"children":578},{"style":502},[579],{"type":31,"value":580},"List orders using cursor pagination\n",{"type":26,"tag":451,"props":582,"children":584},{"class":453,"line":583},9,[585,590],{"type":26,"tag":451,"props":586,"children":587},{"style":458},[588],{"type":31,"value":589},"      responses",{"type":26,"tag":451,"props":591,"children":592},{"style":463},[593],{"type":31,"value":486},{"type":26,"tag":451,"props":595,"children":597},{"class":453,"line":596},10,[598,603],{"type":26,"tag":451,"props":599,"children":600},{"style":502},[601],{"type":31,"value":602},"        '200'",{"type":26,"tag":451,"props":604,"children":605},{"style":463},[606],{"type":31,"value":486},{"type":26,"tag":451,"props":608,"children":610},{"class":453,"line":609},11,[611,616,620],{"type":26,"tag":451,"props":612,"children":613},{"style":458},[614],{"type":31,"value":615},"          description",{"type":26,"tag":451,"props":617,"children":618},{"style":463},[619],{"type":31,"value":466},{"type":26,"tag":451,"props":621,"children":622},{"style":502},[623],{"type":31,"value":624},"An envelope containing items and the next cursor\n",{"type":26,"tag":27,"props":626,"children":627},{},[628,630,635,637,643,645,650,652,659],{"type":31,"value":629},"This is a shortened document fragment. ",{"type":26,"tag":15,"props":631,"children":633},{"className":632},[],[634],{"type":31,"value":18},{"type":31,"value":636}," identifies the specification format; ",{"type":26,"tag":15,"props":638,"children":640},{"className":639},[],[641],{"type":31,"value":642},"info.version",{"type":31,"value":644}," versions the API document. Neither field implements request routing or changes a deployed client's behavior. The ",{"type":26,"tag":15,"props":646,"children":648},{"className":647},[],[649],{"type":31,"value":350},{"type":31,"value":651}," path needs an implementation. The ",{"type":26,"tag":93,"props":653,"children":656},{"href":654,"rel":655},"https://spec.openapis.org/oas/v3.1.0.html#info-object",[97],[657],{"type":31,"value":658},"OpenAPI Info Object definition",{"type":31,"value":660}," makes the distinction explicit. An SDK's package version is another separate lifecycle: document which server contracts it supports.",{"type":26,"tag":55,"props":662,"children":664},{"id":663},"a-small-response-change-that-breaks-a-real-client-shape",[665],{"type":31,"value":666},"A small response change that breaks a real client shape",{"type":26,"tag":27,"props":668,"children":669},{},[670,672,678],{"type":31,"value":671},"Suppose an existing endpoint supports offset pagination. A request to ",{"type":26,"tag":15,"props":673,"children":675},{"className":674},[],[676],{"type":31,"value":677},"GET /v1/orders?limit=2&offset=0",{"type":31,"value":679}," returns a JSON array:",{"type":26,"tag":441,"props":681,"children":685},{"className":682,"code":683,"language":684,"meta":8,"style":8},"language-json shiki shiki-themes github-dark github-light","[\n  { \"id\": \"ord_102\", \"status\": \"CONFIRMED\" },\n  { \"id\": \"ord_101\", \"status\": \"PENDING\" }\n]\n","json",[686],{"type":26,"tag":15,"props":687,"children":688},{"__ignoreMap":8},[689,697,743,785],{"type":26,"tag":451,"props":690,"children":691},{"class":453,"line":454},[692],{"type":26,"tag":451,"props":693,"children":694},{"style":463},[695],{"type":31,"value":696},"[\n",{"type":26,"tag":451,"props":698,"children":699},{"class":453,"line":475},[700,705,710,714,719,724,729,733,738],{"type":26,"tag":451,"props":701,"children":702},{"style":463},[703],{"type":31,"value":704},"  { ",{"type":26,"tag":451,"props":706,"children":707},{"style":469},[708],{"type":31,"value":709},"\"id\"",{"type":26,"tag":451,"props":711,"children":712},{"style":463},[713],{"type":31,"value":466},{"type":26,"tag":451,"props":715,"children":716},{"style":502},[717],{"type":31,"value":718},"\"ord_102\"",{"type":26,"tag":451,"props":720,"children":721},{"style":463},[722],{"type":31,"value":723},", ",{"type":26,"tag":451,"props":725,"children":726},{"style":469},[727],{"type":31,"value":728},"\"status\"",{"type":26,"tag":451,"props":730,"children":731},{"style":463},[732],{"type":31,"value":466},{"type":26,"tag":451,"props":734,"children":735},{"style":502},[736],{"type":31,"value":737},"\"CONFIRMED\"",{"type":26,"tag":451,"props":739,"children":740},{"style":463},[741],{"type":31,"value":742}," },\n",{"type":26,"tag":451,"props":744,"children":745},{"class":453,"line":489},[746,750,754,758,763,767,771,775,780],{"type":26,"tag":451,"props":747,"children":748},{"style":463},[749],{"type":31,"value":704},{"type":26,"tag":451,"props":751,"children":752},{"style":469},[753],{"type":31,"value":709},{"type":26,"tag":451,"props":755,"children":756},{"style":463},[757],{"type":31,"value":466},{"type":26,"tag":451,"props":759,"children":760},{"style":502},[761],{"type":31,"value":762},"\"ord_101\"",{"type":26,"tag":451,"props":764,"children":765},{"style":463},[766],{"type":31,"value":723},{"type":26,"tag":451,"props":768,"children":769},{"style":469},[770],{"type":31,"value":728},{"type":26,"tag":451,"props":772,"children":773},{"style":463},[774],{"type":31,"value":466},{"type":26,"tag":451,"props":776,"children":777},{"style":502},[778],{"type":31,"value":779},"\"PENDING\"",{"type":26,"tag":451,"props":781,"children":782},{"style":463},[783],{"type":31,"value":784}," }\n",{"type":26,"tag":451,"props":786,"children":787},{"class":453,"line":508},[788],{"type":26,"tag":451,"props":789,"children":790},{"style":463},[791],{"type":31,"value":792},"]\n",{"type":26,"tag":27,"props":794,"children":795},{},[796],{"type":31,"value":797},"A client might contain this perfectly reasonable code:",{"type":26,"tag":441,"props":799,"children":803},{"className":800,"code":801,"language":802,"meta":8,"style":8},"language-javascript shiki shiki-themes github-dark github-light","function orderIdsFromV1(body) {\n  return body.map(order => order.id);\n}\n","javascript",[804],{"type":26,"tag":15,"props":805,"children":806},{"__ignoreMap":8},[807,838,875],{"type":26,"tag":451,"props":808,"children":809},{"class":453,"line":454},[810,816,822,827,833],{"type":26,"tag":451,"props":811,"children":813},{"style":812},"--shiki-default:#F97583;--shiki-light:#D73A49",[814],{"type":31,"value":815},"function",{"type":26,"tag":451,"props":817,"children":819},{"style":818},"--shiki-default:#B392F0;--shiki-light:#6F42C1",[820],{"type":31,"value":821}," orderIdsFromV1",{"type":26,"tag":451,"props":823,"children":824},{"style":463},[825],{"type":31,"value":826},"(",{"type":26,"tag":451,"props":828,"children":830},{"style":829},"--shiki-default:#FFAB70;--shiki-light:#E36209",[831],{"type":31,"value":832},"body",{"type":26,"tag":451,"props":834,"children":835},{"style":463},[836],{"type":31,"value":837},") {\n",{"type":26,"tag":451,"props":839,"children":840},{"class":453,"line":475},[841,846,851,856,860,865,870],{"type":26,"tag":451,"props":842,"children":843},{"style":812},[844],{"type":31,"value":845},"  return",{"type":26,"tag":451,"props":847,"children":848},{"style":463},[849],{"type":31,"value":850}," body.",{"type":26,"tag":451,"props":852,"children":853},{"style":818},[854],{"type":31,"value":855},"map",{"type":26,"tag":451,"props":857,"children":858},{"style":463},[859],{"type":31,"value":826},{"type":26,"tag":451,"props":861,"children":862},{"style":829},[863],{"type":31,"value":864},"order",{"type":26,"tag":451,"props":866,"children":867},{"style":812},[868],{"type":31,"value":869}," =>",{"type":26,"tag":451,"props":871,"children":872},{"style":463},[873],{"type":31,"value":874}," order.id);\n",{"type":26,"tag":451,"props":876,"children":877},{"class":453,"line":489},[878],{"type":26,"tag":451,"props":879,"children":880},{"style":463},[881],{"type":31,"value":882},"}\n",{"type":26,"tag":27,"props":884,"children":885},{},[886],{"type":31,"value":887},"You want to introduce cursor pagination and a place for pagination metadata. The proposed response becomes:",{"type":26,"tag":441,"props":889,"children":891},{"className":682,"code":890,"language":684,"meta":8,"style":8},"{\n  \"items\": [\n    { \"id\": \"ord_102\", \"status\": \"CONFIRMED\" },\n    { \"id\": \"ord_101\", \"status\": \"PENDING\" }\n  ],\n  \"nextCursor\": \"opaque-continuation-token\"\n}\n",[892],{"type":26,"tag":15,"props":893,"children":894},{"__ignoreMap":8},[895,903,916,956,995,1003,1020],{"type":26,"tag":451,"props":896,"children":897},{"class":453,"line":454},[898],{"type":26,"tag":451,"props":899,"children":900},{"style":463},[901],{"type":31,"value":902},"{\n",{"type":26,"tag":451,"props":904,"children":905},{"class":453,"line":475},[906,911],{"type":26,"tag":451,"props":907,"children":908},{"style":469},[909],{"type":31,"value":910},"  \"items\"",{"type":26,"tag":451,"props":912,"children":913},{"style":463},[914],{"type":31,"value":915},": [\n",{"type":26,"tag":451,"props":917,"children":918},{"class":453,"line":489},[919,924,928,932,936,940,944,948,952],{"type":26,"tag":451,"props":920,"children":921},{"style":463},[922],{"type":31,"value":923},"    { ",{"type":26,"tag":451,"props":925,"children":926},{"style":469},[927],{"type":31,"value":709},{"type":26,"tag":451,"props":929,"children":930},{"style":463},[931],{"type":31,"value":466},{"type":26,"tag":451,"props":933,"children":934},{"style":502},[935],{"type":31,"value":718},{"type":26,"tag":451,"props":937,"children":938},{"style":463},[939],{"type":31,"value":723},{"type":26,"tag":451,"props":941,"children":942},{"style":469},[943],{"type":31,"value":728},{"type":26,"tag":451,"props":945,"children":946},{"style":463},[947],{"type":31,"value":466},{"type":26,"tag":451,"props":949,"children":950},{"style":502},[951],{"type":31,"value":737},{"type":26,"tag":451,"props":953,"children":954},{"style":463},[955],{"type":31,"value":742},{"type":26,"tag":451,"props":957,"children":958},{"class":453,"line":508},[959,963,967,971,975,979,983,987,991],{"type":26,"tag":451,"props":960,"children":961},{"style":463},[962],{"type":31,"value":923},{"type":26,"tag":451,"props":964,"children":965},{"style":469},[966],{"type":31,"value":709},{"type":26,"tag":451,"props":968,"children":969},{"style":463},[970],{"type":31,"value":466},{"type":26,"tag":451,"props":972,"children":973},{"style":502},[974],{"type":31,"value":762},{"type":26,"tag":451,"props":976,"children":977},{"style":463},[978],{"type":31,"value":723},{"type":26,"tag":451,"props":980,"children":981},{"style":469},[982],{"type":31,"value":728},{"type":26,"tag":451,"props":984,"children":985},{"style":463},[986],{"type":31,"value":466},{"type":26,"tag":451,"props":988,"children":989},{"style":502},[990],{"type":31,"value":779},{"type":26,"tag":451,"props":992,"children":993},{"style":463},[994],{"type":31,"value":784},{"type":26,"tag":451,"props":996,"children":997},{"class":453,"line":526},[998],{"type":26,"tag":451,"props":999,"children":1000},{"style":463},[1001],{"type":31,"value":1002},"  ],\n",{"type":26,"tag":451,"props":1004,"children":1005},{"class":453,"line":539},[1006,1011,1015],{"type":26,"tag":451,"props":1007,"children":1008},{"style":469},[1009],{"type":31,"value":1010},"  \"nextCursor\"",{"type":26,"tag":451,"props":1012,"children":1013},{"style":463},[1014],{"type":31,"value":466},{"type":26,"tag":451,"props":1016,"children":1017},{"style":502},[1018],{"type":31,"value":1019},"\"opaque-continuation-token\"\n",{"type":26,"tag":451,"props":1021,"children":1022},{"class":453,"line":552},[1023],{"type":26,"tag":451,"props":1024,"children":1025},{"style":463},[1026],{"type":31,"value":882},{"type":26,"tag":27,"props":1028,"children":1029},{},[1030,1032,1038],{"type":31,"value":1031},"The old client now fails at ",{"type":26,"tag":15,"props":1033,"children":1035},{"className":1034},[],[1036],{"type":31,"value":1037},"body.map(...)",{"type":31,"value":1039},". Keeping every order field did not preserve compatibility: the response root changed from an array to an object.",{"type":26,"tag":27,"props":1041,"children":1042},{},[1043,1045,1051,1053,1059,1061,1067,1069,1075],{"type":31,"value":1044},"Offer that representation at ",{"type":26,"tag":15,"props":1046,"children":1048},{"className":1047},[],[1049],{"type":31,"value":1050},"GET /v2/orders?limit=2",{"type":31,"value":1052},", while ",{"type":26,"tag":15,"props":1054,"children":1056},{"className":1055},[],[1057],{"type":31,"value":1058},"/v1/orders",{"type":31,"value":1060}," keeps its existing shape and pagination behavior. A migrated client reads ",{"type":26,"tag":15,"props":1062,"children":1064},{"className":1063},[],[1065],{"type":31,"value":1066},"body.items",{"type":31,"value":1068}," and supplies the returned cursor on its next request. In this example, ",{"type":26,"tag":15,"props":1070,"children":1072},{"className":1071},[],[1073],{"type":31,"value":1074},"nextCursor: null",{"type":31,"value":1076}," means there is no next page; the token shown above is illustrative.",{"type":26,"tag":27,"props":1078,"children":1079},{},[1080],{"type":31,"value":1081},"Define the v2 pagination contract precisely: maximum and default page sizes, deterministic ordering with a unique tie-breaker, token opacity and expiry, which filters must remain unchanged, and what concurrent inserts or updates mean for traversal. Cursor pagination does not by itself promise a snapshot of the dataset. Clients need to know whether an export requires a separate snapshot mechanism.",{"type":26,"tag":27,"props":1083,"children":1084},{},[1085],{"type":31,"value":1086},"Use version-specific request and response adapters around shared application logic where practical. A new HTTP representation does not require copying the entire service. It does require preserving each supported contract's behavior, including its pagination rules. A v1 client should not start receiving cursor semantics simply because the internal query implementation changed.",{"type":26,"tag":55,"props":1088,"children":1090},{"id":1089},"deploy-support-first-migrate-clients-gradually",[1091],{"type":31,"value":1092},"Deploy support first, migrate clients gradually",{"type":26,"tag":27,"props":1094,"children":1095},{},[1096],{"type":31,"value":1097},"A migration should have valid intermediate states. For the order example, I would plan three provider stages:",{"type":26,"tag":1099,"props":1100,"children":1101},"ol",{},[1102,1113,1123],{"type":26,"tag":1103,"props":1104,"children":1105},"li",{},[1106,1111],{"type":26,"tag":42,"props":1107,"children":1108},{},[1109],{"type":31,"value":1110},"Expand:",{"type":31,"value":1112}," deploy a release that serves both v1 and v2. Keep v1 working, and make v2 available in a test environment with documentation and examples. Complete the production rollout before inviting clients to depend on v2, or route v2 traffic only to instances that support it.",{"type":26,"tag":1103,"props":1114,"children":1115},{},[1116,1121],{"type":26,"tag":42,"props":1117,"children":1118},{},[1119],{"type":31,"value":1120},"Migrate:",{"type":31,"value":1122}," clients test and switch on their own schedules within the published support window. Observe traffic and failures by API version and authenticated integration identity. Maintain both contracts while the migration is active.",{"type":26,"tag":1103,"props":1124,"children":1125},{},[1126,1131],{"type":26,"tag":42,"props":1127,"children":1128},{},[1129],{"type":31,"value":1130},"Retire:",{"type":31,"value":1132}," remove v1 only after the announced policy and migration criteria have been met. Retirement is a separate operational decision from introducing v2.",{"type":26,"tag":27,"props":1134,"children":1135},{},[1136],{"type":31,"value":1137},"The compatibility matrix makes the deployment boundary visible:",{"type":26,"tag":125,"props":1139,"children":1140},{},[1141,1172],{"type":26,"tag":129,"props":1142,"children":1143},{},[1144],{"type":26,"tag":133,"props":1145,"children":1146},{},[1147,1152,1157,1162,1167],{"type":26,"tag":137,"props":1148,"children":1149},{},[1150],{"type":31,"value":1151},"Client behavior",{"type":26,"tag":137,"props":1153,"children":1154},{},[1155],{"type":31,"value":1156},"Original server: v1 only",{"type":26,"tag":137,"props":1158,"children":1159},{},[1160],{"type":31,"value":1161},"Compatibility release: v1 + v2",{"type":26,"tag":137,"props":1163,"children":1164},{},[1165],{"type":31,"value":1166},"Later release: v1 + v2",{"type":26,"tag":137,"props":1168,"children":1169},{},[1170],{"type":31,"value":1171},"After v1 retirement: v2 only",{"type":26,"tag":153,"props":1173,"children":1174},{},[1175,1202],{"type":26,"tag":133,"props":1176,"children":1177},{},[1178,1183,1188,1193,1197],{"type":26,"tag":160,"props":1179,"children":1180},{},[1181],{"type":31,"value":1182},"Existing client uses v1",{"type":26,"tag":160,"props":1184,"children":1185},{},[1186],{"type":31,"value":1187},"Works",{"type":26,"tag":160,"props":1189,"children":1190},{},[1191],{"type":31,"value":1192},"Must work",{"type":26,"tag":160,"props":1194,"children":1195},{},[1196],{"type":31,"value":1192},{"type":26,"tag":160,"props":1198,"children":1199},{},[1200],{"type":31,"value":1201},"Unsupported",{"type":26,"tag":133,"props":1203,"children":1204},{},[1205,1210,1214,1218,1222],{"type":26,"tag":160,"props":1206,"children":1207},{},[1208],{"type":31,"value":1209},"Migrated client uses v2",{"type":26,"tag":160,"props":1211,"children":1212},{},[1213],{"type":31,"value":1201},{"type":26,"tag":160,"props":1215,"children":1216},{},[1217],{"type":31,"value":1192},{"type":26,"tag":160,"props":1219,"children":1220},{},[1221],{"type":31,"value":1192},{"type":26,"tag":160,"props":1223,"children":1224},{},[1225],{"type":31,"value":1187},{"type":26,"tag":27,"props":1227,"children":1228},{},[1229,1231,1236],{"type":31,"value":1230},"The second row explains why “we can always roll back to the previous version” is incomplete. ",{"type":26,"tag":42,"props":1232,"children":1233},{},[1234],{"type":31,"value":1235},"Once clients depend on v2, a server that only supports v1 is no longer a safe general rollback target.",{"type":31,"value":1237}," Keep a known-good release supporting both contracts, test recovery to that baseline, and retain the supporting infrastructure and data shape.",{"type":26,"tag":27,"props":1239,"children":1240},{},[1241],{"type":31,"value":1242},"If a new client deliberately supports falling back to v1, test that path. Do not assume fallback exists, and do not silently retry a state-changing request against another version: an ambiguous response may hide an operation that already succeeded. Recovery must preserve the operation's semantics and idempotency rules.",{"type":26,"tag":27,"props":1244,"children":1245},{},[1246],{"type":31,"value":1247},"A shared database can impose another rollback boundary. If an API release writes values that the older binary cannot interpret, preserving the HTTP route is insufficient. Keep schema and data changes compatible with the supported rollback baseline, backfill when needed, and defer destructive cleanup until the old readers and rollback window have been retired.",{"type":26,"tag":27,"props":1249,"children":1250},{},[1251],{"type":31,"value":1252},"Feature flags help control exposure, but disabling a feature that clients already require can still break them. Once a public capability is in use, recovery needs to respect the contract it established.",{"type":26,"tag":55,"props":1254,"children":1256},{"id":1255},"give-clients-a-migration-path-they-can-actually-follow",[1257],{"type":31,"value":1258},"Give clients a migration path they can actually follow",{"type":26,"tag":27,"props":1260,"children":1261},{},[1262],{"type":31,"value":1263},"A release note saying “v1 is deprecated; use v2” leaves most of the work with the consumer. Publish the request and response differences, concrete before-and-after examples, changed defaults and errors, SDK guidance, the support timeline, and a way to raise migration problems.",{"type":26,"tag":27,"props":1265,"children":1266},{},[1267,1269,1275,1277,1283,1285,1291,1293,1299],{"type":31,"value":1268},"For this example, show the actual client edits: read ",{"type":26,"tag":15,"props":1270,"children":1272},{"className":1271},[],[1273],{"type":31,"value":1274},"items",{"type":31,"value":1276},", stop sending ",{"type":26,"tag":15,"props":1278,"children":1280},{"className":1279},[],[1281],{"type":31,"value":1282},"offset",{"type":31,"value":1284},", persist the opaque cursor only for the intended traversal, and stop when ",{"type":26,"tag":15,"props":1286,"children":1288},{"className":1287},[],[1289],{"type":31,"value":1290},"nextCursor",{"type":31,"value":1292}," is ",{"type":26,"tag":15,"props":1294,"children":1296},{"className":1295},[],[1297],{"type":31,"value":1298},"null",{"type":31,"value":1300},". Explain the behavior for an expired cursor. Let clients try this against a representative test environment before switching production traffic.",{"type":26,"tag":27,"props":1302,"children":1303},{},[1304],{"type":31,"value":1305},"HTTP headers can make the lifecycle discoverable. Here is a fictional response announcing deprecation on 1 June 2025 and planned retirement on 1 December 2025:",{"type":26,"tag":441,"props":1307,"children":1310},{"className":1308,"code":1309,"language":31,"meta":8,"style":8},"language-text shiki shiki-themes github-dark github-light","HTTP/1.1 200 OK\nContent-Type: application/json\nDeprecation: @1748736000\nSunset: Mon, 01 Dec 2025 00:00:00 GMT\nLink: \u003Chttps://api.example.com/docs/migrations/orders-v2>; rel=\"deprecation\"; type=\"text/html\"\n",[1311],{"type":26,"tag":15,"props":1312,"children":1313},{"__ignoreMap":8},[1314,1322,1330,1338,1346],{"type":26,"tag":451,"props":1315,"children":1316},{"class":453,"line":454},[1317],{"type":26,"tag":451,"props":1318,"children":1319},{},[1320],{"type":31,"value":1321},"HTTP/1.1 200 OK\n",{"type":26,"tag":451,"props":1323,"children":1324},{"class":453,"line":475},[1325],{"type":26,"tag":451,"props":1326,"children":1327},{},[1328],{"type":31,"value":1329},"Content-Type: application/json\n",{"type":26,"tag":451,"props":1331,"children":1332},{"class":453,"line":489},[1333],{"type":26,"tag":451,"props":1334,"children":1335},{},[1336],{"type":31,"value":1337},"Deprecation: @1748736000\n",{"type":26,"tag":451,"props":1339,"children":1340},{"class":453,"line":508},[1341],{"type":26,"tag":451,"props":1342,"children":1343},{},[1344],{"type":31,"value":1345},"Sunset: Mon, 01 Dec 2025 00:00:00 GMT\n",{"type":26,"tag":451,"props":1347,"children":1348},{"class":453,"line":526},[1349],{"type":26,"tag":451,"props":1350,"children":1351},{},[1352],{"type":31,"value":1353},"Link: \u003Chttps://api.example.com/docs/migrations/orders-v2>; rel=\"deprecation\"; type=\"text/html\"\n",{"type":26,"tag":27,"props":1355,"children":1356},{},[1357,1363,1365,1371,1373,1380,1382,1388,1390,1396,1398,1405],{"type":26,"tag":15,"props":1358,"children":1360},{"className":1359},[],[1361],{"type":31,"value":1362},"Deprecation",{"type":31,"value":1364}," uses a Structured Field date: ",{"type":26,"tag":15,"props":1366,"children":1368},{"className":1367},[],[1369],{"type":31,"value":1370},"@",{"type":31,"value":1372}," followed by Unix seconds. It is not a Boolean or an HTTP-date. ",{"type":26,"tag":93,"props":1374,"children":1377},{"href":1375,"rel":1376},"https://www.rfc-editor.org/rfc/rfc9745.html",[97],[1378],{"type":31,"value":1379},"RFC 9745",{"type":31,"value":1381}," defines that signal and the ",{"type":26,"tag":15,"props":1383,"children":1385},{"className":1384},[],[1386],{"type":31,"value":1387},"deprecation",{"type":31,"value":1389}," link relation. ",{"type":26,"tag":15,"props":1391,"children":1393},{"className":1392},[],[1394],{"type":31,"value":1395},"Sunset",{"type":31,"value":1397}," uses an HTTP-date and announces when the resource is expected to become unavailable; it does not prescribe the response after retirement. See ",{"type":26,"tag":93,"props":1399,"children":1402},{"href":1400,"rel":1401},"https://www.rfc-editor.org/rfc/rfc8594.html",[97],[1403],{"type":31,"value":1404},"RFC 8594",{"type":31,"value":1406},". Deprecation and shutdown are separate events; a deprecation notice alone is not a shutdown instruction.",{"type":26,"tag":27,"props":1408,"children":1409},{},[1410],{"type":31,"value":1411},"Headers complement communication. They do not guarantee a human has seen the notice. Use the channels clients agreed to receive, and track migration progress. A quiet week does not prove that an integration is unused: monthly jobs, seasonal workloads, and recovery tools may not appear in that window.",{"type":26,"tag":27,"props":1413,"children":1414},{},[1415],{"type":31,"value":1416},"Choose a support period that matches your clients' release constraints, then publish it and monitor it. The dates above illustrate the header formats, not a universal six-month policy. Supporting old versions has a cost, so make retirement deliberate rather than either indefinite by accident or abrupt for consumers.",{"type":26,"tag":55,"props":1418,"children":1420},{"id":1419},"test-the-combinations-you-intend-to-support",[1421],{"type":31,"value":1422},"Test the combinations you intend to support",{"type":26,"tag":27,"props":1424,"children":1425},{},[1426],{"type":31,"value":1427},"Keep the published contract as a baseline and review schema changes in CI. A structural diff can flag a removed field or a newly required input. It cannot prove that an unchanged numeric field still uses the same units, that authorization scopes remain sufficient, or that ordering and errors retain their meaning.",{"type":26,"tag":27,"props":1429,"children":1430},{},[1431],{"type":31,"value":1432},"For the migration above, preserve fixtures and tests that exercise these expectations:",{"type":26,"tag":125,"props":1434,"children":1435},{},[1436,1452],{"type":26,"tag":129,"props":1437,"children":1438},{},[1439],{"type":26,"tag":133,"props":1440,"children":1441},{},[1442,1447],{"type":26,"tag":137,"props":1443,"children":1444},{},[1445],{"type":31,"value":1446},"Check",{"type":26,"tag":137,"props":1448,"children":1449},{},[1450],{"type":31,"value":1451},"What it protects",{"type":26,"tag":153,"props":1453,"children":1454},{},[1455,1468,1481,1494,1507],{"type":26,"tag":133,"props":1456,"children":1457},{},[1458,1463],{"type":26,"tag":160,"props":1459,"children":1460},{},[1461],{"type":31,"value":1462},"Old requests against the new provider",{"type":26,"tag":160,"props":1464,"children":1465},{},[1466],{"type":31,"value":1467},"Previously valid inputs and defaults still work",{"type":26,"tag":133,"props":1469,"children":1470},{},[1471,1476],{"type":26,"tag":160,"props":1472,"children":1473},{},[1474],{"type":31,"value":1475},"Old client decoder against new v1 responses",{"type":26,"tag":160,"props":1477,"children":1478},{},[1479],{"type":31,"value":1480},"Existing consumers still understand the representation",{"type":26,"tag":133,"props":1482,"children":1483},{},[1484,1489],{"type":26,"tag":160,"props":1485,"children":1486},{},[1487],{"type":31,"value":1488},"v2 traversal over multiple pages",{"type":26,"tag":160,"props":1490,"children":1491},{},[1492],{"type":31,"value":1493},"The envelope, cursor termination, ordering, and invalid-token behavior match the documented contract",{"type":26,"tag":133,"props":1495,"children":1496},{},[1497,1502],{"type":26,"tag":160,"props":1498,"children":1499},{},[1500],{"type":31,"value":1501},"Known-good rollback release against current data",{"type":26,"tag":160,"props":1503,"children":1504},{},[1505],{"type":31,"value":1506},"Recovery remains possible after new writes and migrations",{"type":26,"tag":133,"props":1508,"children":1509},{},[1510,1515],{"type":26,"tag":160,"props":1511,"children":1512},{},[1513],{"type":31,"value":1514},"Both versions during the overlap period",{"type":26,"tag":160,"props":1516,"children":1517},{},[1518],{"type":31,"value":1519},"A refactor shared by the adapters does not silently break one version",{"type":26,"tag":27,"props":1521,"children":1522},{},[1523],{"type":31,"value":1524},"A minimal JavaScript regression check can reuse the old decoder instead of replacing it with the new one. Assume the test server has a controlled fixture containing the two orders shown earlier:",{"type":26,"tag":441,"props":1526,"children":1528},{"className":800,"code":1527,"language":802,"meta":8,"style":8},"import assert from 'node:assert/strict';\n\nconst response = await fetch(\n  'http://localhost:8080/v1/orders?limit=2&offset=0'\n);\nassert.equal(response.status, 200);\n\nconst body = await response.json();\nassert.ok(Array.isArray(body));\nassert.deepEqual(orderIdsFromV1(body), ['ord_102', 'ord_101']);\n",[1529],{"type":26,"tag":15,"props":1530,"children":1531},{"__ignoreMap":8},[1532,1560,1569,1602,1610,1618,1645,1652,1686,1713],{"type":26,"tag":451,"props":1533,"children":1534},{"class":453,"line":454},[1535,1540,1545,1550,1555],{"type":26,"tag":451,"props":1536,"children":1537},{"style":812},[1538],{"type":31,"value":1539},"import",{"type":26,"tag":451,"props":1541,"children":1542},{"style":463},[1543],{"type":31,"value":1544}," assert ",{"type":26,"tag":451,"props":1546,"children":1547},{"style":812},[1548],{"type":31,"value":1549},"from",{"type":26,"tag":451,"props":1551,"children":1552},{"style":502},[1553],{"type":31,"value":1554}," 'node:assert/strict'",{"type":26,"tag":451,"props":1556,"children":1557},{"style":463},[1558],{"type":31,"value":1559},";\n",{"type":26,"tag":451,"props":1561,"children":1562},{"class":453,"line":475},[1563],{"type":26,"tag":451,"props":1564,"children":1566},{"emptyLinePlaceholder":1565},true,[1567],{"type":31,"value":1568},"\n",{"type":26,"tag":451,"props":1570,"children":1571},{"class":453,"line":489},[1572,1577,1582,1587,1592,1597],{"type":26,"tag":451,"props":1573,"children":1574},{"style":812},[1575],{"type":31,"value":1576},"const",{"type":26,"tag":451,"props":1578,"children":1579},{"style":469},[1580],{"type":31,"value":1581}," response",{"type":26,"tag":451,"props":1583,"children":1584},{"style":812},[1585],{"type":31,"value":1586}," =",{"type":26,"tag":451,"props":1588,"children":1589},{"style":812},[1590],{"type":31,"value":1591}," await",{"type":26,"tag":451,"props":1593,"children":1594},{"style":818},[1595],{"type":31,"value":1596}," fetch",{"type":26,"tag":451,"props":1598,"children":1599},{"style":463},[1600],{"type":31,"value":1601},"(\n",{"type":26,"tag":451,"props":1603,"children":1604},{"class":453,"line":508},[1605],{"type":26,"tag":451,"props":1606,"children":1607},{"style":502},[1608],{"type":31,"value":1609},"  'http://localhost:8080/v1/orders?limit=2&offset=0'\n",{"type":26,"tag":451,"props":1611,"children":1612},{"class":453,"line":526},[1613],{"type":26,"tag":451,"props":1614,"children":1615},{"style":463},[1616],{"type":31,"value":1617},");\n",{"type":26,"tag":451,"props":1619,"children":1620},{"class":453,"line":539},[1621,1626,1631,1636,1641],{"type":26,"tag":451,"props":1622,"children":1623},{"style":463},[1624],{"type":31,"value":1625},"assert.",{"type":26,"tag":451,"props":1627,"children":1628},{"style":818},[1629],{"type":31,"value":1630},"equal",{"type":26,"tag":451,"props":1632,"children":1633},{"style":463},[1634],{"type":31,"value":1635},"(response.status, ",{"type":26,"tag":451,"props":1637,"children":1638},{"style":469},[1639],{"type":31,"value":1640},"200",{"type":26,"tag":451,"props":1642,"children":1643},{"style":463},[1644],{"type":31,"value":1617},{"type":26,"tag":451,"props":1646,"children":1647},{"class":453,"line":552},[1648],{"type":26,"tag":451,"props":1649,"children":1650},{"emptyLinePlaceholder":1565},[1651],{"type":31,"value":1568},{"type":26,"tag":451,"props":1653,"children":1654},{"class":453,"line":565},[1655,1659,1664,1668,1672,1677,1681],{"type":26,"tag":451,"props":1656,"children":1657},{"style":812},[1658],{"type":31,"value":1576},{"type":26,"tag":451,"props":1660,"children":1661},{"style":469},[1662],{"type":31,"value":1663}," body",{"type":26,"tag":451,"props":1665,"children":1666},{"style":812},[1667],{"type":31,"value":1586},{"type":26,"tag":451,"props":1669,"children":1670},{"style":812},[1671],{"type":31,"value":1591},{"type":26,"tag":451,"props":1673,"children":1674},{"style":463},[1675],{"type":31,"value":1676}," response.",{"type":26,"tag":451,"props":1678,"children":1679},{"style":818},[1680],{"type":31,"value":684},{"type":26,"tag":451,"props":1682,"children":1683},{"style":463},[1684],{"type":31,"value":1685},"();\n",{"type":26,"tag":451,"props":1687,"children":1688},{"class":453,"line":583},[1689,1693,1698,1703,1708],{"type":26,"tag":451,"props":1690,"children":1691},{"style":463},[1692],{"type":31,"value":1625},{"type":26,"tag":451,"props":1694,"children":1695},{"style":818},[1696],{"type":31,"value":1697},"ok",{"type":26,"tag":451,"props":1699,"children":1700},{"style":463},[1701],{"type":31,"value":1702},"(Array.",{"type":26,"tag":451,"props":1704,"children":1705},{"style":818},[1706],{"type":31,"value":1707},"isArray",{"type":26,"tag":451,"props":1709,"children":1710},{"style":463},[1711],{"type":31,"value":1712},"(body));\n",{"type":26,"tag":451,"props":1714,"children":1715},{"class":453,"line":596},[1716,1720,1725,1729,1734,1739,1744,1748,1753],{"type":26,"tag":451,"props":1717,"children":1718},{"style":463},[1719],{"type":31,"value":1625},{"type":26,"tag":451,"props":1721,"children":1722},{"style":818},[1723],{"type":31,"value":1724},"deepEqual",{"type":26,"tag":451,"props":1726,"children":1727},{"style":463},[1728],{"type":31,"value":826},{"type":26,"tag":451,"props":1730,"children":1731},{"style":818},[1732],{"type":31,"value":1733},"orderIdsFromV1",{"type":26,"tag":451,"props":1735,"children":1736},{"style":463},[1737],{"type":31,"value":1738},"(body), [",{"type":26,"tag":451,"props":1740,"children":1741},{"style":502},[1742],{"type":31,"value":1743},"'ord_102'",{"type":26,"tag":451,"props":1745,"children":1746},{"style":463},[1747],{"type":31,"value":723},{"type":26,"tag":451,"props":1749,"children":1750},{"style":502},[1751],{"type":31,"value":1752},"'ord_101'",{"type":26,"tag":451,"props":1754,"children":1755},{"style":463},[1756],{"type":31,"value":1757},"]);\n",{"type":26,"tag":27,"props":1759,"children":1760},{},[1761,1763,1768],{"type":31,"value":1762},"Here ",{"type":26,"tag":15,"props":1764,"children":1766},{"className":1765},[],[1767],{"type":31,"value":1733},{"type":31,"value":1769}," is the unchanged function shown above. Run the check against the candidate provider with deterministic test data and the authentication required by your API. It catches the envelope change; broader tests must cover validation, error handling, pagination, and semantics. Do not update the old compatibility fixture just to make an incompatible release pass.",{"type":26,"tag":27,"props":1771,"children":1772},{},[1773,1775,1782],{"type":31,"value":1774},"Consumer-driven contract testing can add evidence from known integrations. Consumers exercise their real client code and publish expected interactions; the provider verifies those interactions against its implementation. ",{"type":26,"tag":93,"props":1776,"children":1779},{"href":1777,"rel":1778},"https://docs.pact.io/getting_started/how_pact_works",[97],[1780],{"type":31,"value":1781},"Pact's workflow",{"type":31,"value":1783}," explains this approach. For a public API, those contracts cover participating consumers, not every client in the world. They support your documented compatibility policy; they do not replace it.",{"type":26,"tag":55,"props":1785,"children":1787},{"id":1786},"treat-independent-deployment-as-a-design-outcome",[1788],{"type":31,"value":1789},"Treat independent deployment as a design outcome",{"type":26,"tag":27,"props":1791,"children":1792},{},[1793],{"type":31,"value":1794},"Teams still need to communicate about API changes. The goal is to give that communication a migration window instead of making it a requirement that everyone presses deploy together.",{"type":26,"tag":27,"props":1796,"children":1797},{},[1798],{"type":31,"value":1799},"Before releasing a change, ask what happens if a client does nothing, if it upgrades next month, or if your new release has to be rolled back. If the only safe answer is a synchronized deployment, revisit the contract and the intermediate states.",{"type":26,"tag":27,"props":1801,"children":1802},{},[1803],{"type":31,"value":1804},"Thinking from the client's side makes those states easier to design. It also makes the API easier to trust: users can keep doing their work while the teams on either side improve the software at their own pace.",{"type":26,"tag":55,"props":1806,"children":1808},{"id":1807},"further-reading",[1809],{"type":31,"value":1810},"Further reading",{"type":26,"tag":1812,"props":1813,"children":1814},"ul",{},[1815,1826,1836,1846,1856],{"type":26,"tag":1103,"props":1816,"children":1817},{},[1818,1824],{"type":26,"tag":93,"props":1819,"children":1821},{"href":95,"rel":1820},[97],[1822],{"type":31,"value":1823},"Google AIP-180: backwards compatibility",{"type":31,"value":1825},".",{"type":26,"tag":1103,"props":1827,"children":1828},{},[1829,1835],{"type":26,"tag":93,"props":1830,"children":1832},{"href":295,"rel":1831},[97],[1833],{"type":31,"value":1834},"Google AIP-185: API versioning",{"type":31,"value":1825},{"type":26,"tag":1103,"props":1837,"children":1838},{},[1839,1845],{"type":26,"tag":93,"props":1840,"children":1842},{"href":1375,"rel":1841},[97],[1843],{"type":31,"value":1844},"RFC 9745: the Deprecation HTTP response header",{"type":31,"value":1825},{"type":26,"tag":1103,"props":1847,"children":1848},{},[1849,1855],{"type":26,"tag":93,"props":1850,"children":1852},{"href":1400,"rel":1851},[97],[1853],{"type":31,"value":1854},"RFC 8594: the Sunset HTTP header",{"type":31,"value":1825},{"type":26,"tag":1103,"props":1857,"children":1858},{},[1859,1865],{"type":26,"tag":93,"props":1860,"children":1862},{"href":1777,"rel":1861},[97],[1863],{"type":31,"value":1864},"Pact: how consumer and provider contract tests work",{"type":31,"value":1825},{"type":26,"tag":1867,"props":1868,"children":1869},"style",{},[1870],{"type":31,"value":1871},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":8,"searchDepth":475,"depth":475,"links":1873},[1874,1875,1876,1877,1878,1879,1880,1881,1882],{"id":57,"depth":475,"text":60},{"id":110,"depth":475,"text":113},{"id":279,"depth":475,"text":282},{"id":663,"depth":475,"text":666},{"id":1089,"depth":475,"text":1092},{"id":1255,"depth":475,"text":1258},{"id":1419,"depth":475,"text":1422},{"id":1786,"depth":475,"text":1789},{"id":1807,"depth":475,"text":1810},"markdown","content:posts:api-evolution-strategy.md","content","posts/api-evolution-strategy.md","posts/api-evolution-strategy","md",1791120245054]