Breaking changes: wat telt en hoe je er een uitbrengt
9 min lezen bijgewerkt op
Een breaking change is een verandering die een correct geschreven aanroeper niet had kunnen overleven. De definitie is belangrijk omdat de meeste discussies over of iets “telt” eigenlijk discussies zijn over wie het verkeerd vasthield. Als een aanroeper jullie documentatie volgde en jullie verandering zorgde dat zijn code ophield te werken, was de verandering breaking. Wat jullie bedoelden heeft daar niets mee te maken.
Dat is de hele test. De rest van dit artikel is wat daaruit voortvloeit: wat de test niet doorstaat, wat hem wel doorstaat, hoe je een falen vangt voordat het wordt gemerged, en wat je doet zodra je weet dat je er een uitbrengt.
Wat telt als een breaking change?
Pas de test toe op de aanroeper, niet op de diff. Een verandering is breaking wanneer een aanroeper die alleen op gedocumenteerd gedrag vertrouwde zijn code, configuratie of data moet aanpassen om te blijven werken. Een veld verwijderen, een endpoint hernoemen, validatie aanscherpen, een standaardwaarde veranderen en het type van een waarde veranderen kwalificeren allemaal. Een optioneel veld toevoegen niet. Een bug fixen meestal niet, met één belangrijke uitzondering hieronder.
| Verandering | Breaking? | Waarom |
|---|---|---|
| Veld, endpoint, flag of optie verwijderen of hernoemen | Ja | Correcte aanroepers verwijzen ernaar |
| Optioneel veld of nieuw endpoint toevoegen | Nee | Bestaande calls veranderen niet |
| Optionele input verplicht maken | Ja | Calls die het weglieten falen nu |
| Eerder geaccepteerde validatie aanscherpen | Ja | Input die werkte wordt nu geweigerd |
| Een standaardwaarde veranderen | Ja | Aanroepers die hem niet instelden krijgen nieuw gedrag |
| Een type veranderen (string naar getal, enkele waarde naar array) | Ja | Parsers geschreven voor het gedocumenteerde type falen |
| Sleutels van een object herordenen | Nee | Tenzij jullie de volgorde documenteerden |
| Een bug fixen waar aanroepers op vertrouwden | In de praktijk ja | Zie de sectie over toevallige contracten |
| Een ratelimiet of groottelimiet verhogen | Nee | Niets wat werkte stopt met werken |
| Een ratelimiet of groottelimiet verlagen | Ja | Verkeer dat prima was wordt nu beperkt |
| De formulering van een foutmelding veranderen | Hangt ervan af | Breaking als jullie het documenteerden of aanroepers erop matchen |
Wat is geen breaking change?
Een verandering is niet-breaking wanneer elke call die eerder werkte nog steeds werkt, ongewijzigd, en nog steeds hetzelfde betekent. Een nieuw endpoint toevoegen, een optionele requestparameter toevoegen, een veld aan een antwoord toevoegen, een verplichte input optioneel maken, een limiet verhogen en een foutmelding verbeteren waar niemand op matcht doorstaan allemaal de test. Deze additieve veranderingen kunnen in een minor release met een gewone changelog-entry.
Additieve veranderingen breken aanroepers toch in drie situaties. Een client waarvan de deserializer onbekende velden weigert, faalt op het eerste nieuwe antwoordveld, dus leg vroeg vast dat aanroepers velden die ze niet kennen moeten negeren. Een nieuwe enum-waarde breekt elke aanroeper met een uitputtende switch (daarover straks meer). En een antwoord dat groeit kan een aanroeper voorbij een groottelimiet, een timeout of een kolombreedte duwen waar hij nooit over hoefde na te denken.
Vier rijen van de tabel verdienen een nadere blik, omdat daar de meningsverschillen ontstaan.
De vier breaking changes die teams missen
Toevallige contracten. Als jullie API drie jaar hetzelfde ongedocumenteerde veld heeft teruggegeven, heeft een aanroeper erop gebouwd. De wet van Hyrum is de korte versie: met genoeg gebruikers zal elk waarneembaar gedrag van jullie systeem afhankelijk zijn voor iemand. Daarom is “het was een bugfix” geen verdediging. De fix kan correct zijn en toch breaking. Breng hem als zodanig uit.
Gedragsveranderingen zonder schemaverandering. Het veld is er nog, het type is hetzelfde, en de
waarde betekent nu iets anders. Een status die vroeger active of inactive was en nu ook
suspended teruggeeft breekt elke aanroeper met een uitputtende switch. Een timestamp die overgaat
van lokale tijd naar UTC breekt iedereen die de docs niet twee keer las. Niets in een diff van het
OpenAPI-bestand toont dit.
Aangescherpte validatie. Jullie beginnen e-mails zonder TLD te weigeren, of achterloopwitruimte, of namen langer dan 80 tekens. Elke aanroeper die precies dat verstuurde krijgt nu een 400 voor een verzoek dat vorige week werkte. Validatieveranderingen worden het vaakst uitgebracht als een “hardening”-fix.
Veranderde standaardwaarden. Niemand die de waarde expliciet instelde merkt iets. Iedereen die dat niet deed, wat de meeste aanroepers zijn, krijgt nieuw gedrag zonder een regel te veranderen. Een veranderde standaardwaarde breekt de meerderheid van jullie gebruikers precies omdat ze de instelling nooit hebben gezien.
Hoe ontdek je een breaking change voordat hij live gaat?
Vergelijk in CI het contract van de pull request met het contract van de hoofdbranch, en laat de build falen bij een breaking verschil. Voor de meeste interfaceformaten bestaan schema-diff-tools, en elk kent de breaking-regels van zijn eigen formaat:
| Interface | Tool | Wat het vergelijkt |
|---|---|---|
| REST (OpenAPI) | oasdiff | Twee OpenAPI-specs, met een rapport over breaking changes |
| gRPC (Protobuf) | buf breaking | .proto-bestanden, op wire- of bronniveau |
| GraphQL | GraphQL Inspector | Twee schema’s, met markering van breaking en gevaarlijke veranderingen |
| Rust-crates | cargo-semver-checks | De publieke API tegen de laatst gepubliceerde versie |
| TypeScript-pakketten | API Extractor | Een gecommit rapport van de publieke API van het pakket |
Deze tools vangen verwijderde velden, hernoemde operaties en veranderde types betrouwbaar. De eerste twee van de vier soorten hierboven, een toevallig contract en een gedragsverandering, zien ze niet, omdat geen van beide in een schema verschijnt. Gebruik de tool om de voor de hand liggende te stoppen en de reviewvraag “kan een correcte aanroeper dit merken?” voor de rest. Dezelfde CI-job is een natuurlijke plek om een changelog-entry te eisen, zoals beschreven in changelog-entries afdwingen in CI, en gRPC- en Protobuf-API-veranderingen loopt de gevallen op wire-niveau door.
Hoe markeer je een breaking change in een commit?
Met Conventional Commits markeer je een breaking
change met een ! voor de dubbele punt (feat(api)!: remove the legacy export endpoint) of met
een footer die begint met BREAKING CHANGE: gevolgd door een beschrijving. Beide leiden tot een
majorversie. Schrijf de footer als eerste concept van de changelog-entry, met wie het treft en wat
ze moeten doen. Conventional commits en de changelog
behandelt hoe ver de conventie je brengt.
Dezelfde regel geldt voor bibliotheken. Een verwijderde publieke functie, een versmald parametertype of een veranderde returnwaarde is een majorversie onder semantische versionering. Bibliotheken volgen dat niet altijd: een studie van 119.879 Maven Central-upgrades vond dat 16,6% de semantische versionering brak, maar slechts 7,9% van de clientprojecten werd getroffen, omdat de meeste van die veranderingen code raakten die geen client aanriep. Breuk wordt gemeten bij de aanroeper.
Hoe breng je een breaking change uit?
Je brengt hem openlijk uit, met een datum, met een pad. De stappen hieronder staan in volgorde, en de laatste is degene die de meeste teams overslaan: de mensen die getroffen werden vertellen dat waar ze op wachtten nu is gebeurd.
- Beslis of het er een is. Gebruik de test hierboven, niet de diff. Als twee engineers het oneens zijn, is het breaking; het oneens-zijn is bewijs dat een aanroeper redelijkerwijs op het oude gedrag had kunnen vertrouwen.
- Versioneer het. Onder semantische versionering is een breaking change een majorversie. Als jullie een gedateerde of geversioneerde API draaien, gaat het in een nieuwe versie en blijft de oude werken tot een genoemde datum. Als jullie niet kunnen versioneren, brengen jullie geen breaking change uit, jullie brengen een storing uit met een changelog-entry. Welk schema de versie draagt is het onderwerp van beste practices voor API-versionering.
- Schrijf de entry voordat de code wordt gemerged. De entry heeft een vaste vorm: wat verandert, wie het treft, wat ze moeten doen, en tegen wanneer. Als jullie niet alle vier kunnen invullen, is de verandering niet klaar. De release notes template zet deze entries vooraan, met een datum in plaats van een versienummer, precies hierom.
- Geef een deadline, geen releasenummer. “Verwijderd in v5” betekent niets voor iemand die jullie releases niet volgt. “Werkt niet meer vanaf 1 november 2026” betekent voor iedereen hetzelfde.
- Lever de migratie mee. De oude call naast de nieuwe. Bij een hernoeming noem je beide namen in dezelfde zin; bij een verwijderd veld zeg je waar de data heen ging.
- Kondig het overal aan waar het oude gedrag was gedocumenteerd. De changelog, de docspagina die het endpoint beschrijft, de release notes van de SDK, en de deprecatie-header in het antwoord als jullie die hebben.
- Sluit de loop. Als een klant om de verandering vroeg, of de bug meldde die ertoe leidde, vertel het haar wanneer het uitkomt.
Hoe ziet een goede breaking-change-entry eruit?
Een goede entry noemt de getroffen aanroeper in de eerste regel, vermeldt de datum, en bevat de fix. Hier is er een voor het geval van aangescherpte validatie, in de vorm die wij gebruiken:
E-mailadressen zonder domein worden geweigerd vanaf 1 november 2026.
POST /usersenPATCH /users/:idaccepteren momenteelalice@localhost. Vanaf 1 november geven deze400 invalid_emailterug. Treft elke integratie die gebruikers aanmaakt vanuit interne directories. Migratie: stuur een volledig gekwalificeerd adres, of laat het veld weg en stel het later in. Geen verandering nodig als jullie adressen al een domein hebben, wat waar is voor 99,4% van de dit jaar aangemaakte accounts.
Waar die melding thuishoort, en wat er nog meer naast moet staan, behandelt de API-changelog.
Het percentage aan het eind is geen decoratie. Het vertelt de lezer of ze zich zorgen moet maken, wat de vraag is waarmee ze de entry opende.
Waarom ze niet gewoon vermijden?
Omdat het alternatief erger is. Een API die nooit iets breekt stapelt elke fout op die hij ooit maakte: het verkeerd benoemde veld, de foute standaardwaarde, de timestamp in lokale tijd. Elk daarvan is een belasting voor elke nieuwe aanroeper voor altijd, om aanroepers te beschermen die in een middag hadden kunnen migreren. De teams met de beste reputatie voor stabiliteit breken dingen zelden, volgens een schema, met een migratiepad en een waarschuwing die de mensen bereikte voor wie het bedoeld was.
De mechaniek van die waarschuwing is het onderwerp van het begeleidende artikel over een API deprecieren. De entry die het aankondigt wordt op dezelfde manier opgesteld als elke andere entry in de changelog-feed: uit de gemergede pull request, vastgehouden voor een mens, dan gepubliceerd op de plek waar de getroffen aanroepers al lezen.
FAQ
Wat is het verschil tussen een breaking en een niet-breaking change? Een breaking change dwingt een correcte aanroeper zijn code, configuratie of data aan te passen om te blijven werken. Een niet-breaking change laat elke bestaande call werken met dezelfde betekenis, daarom zijn toevoegingen meestal veilig en verwijderingen, hernoemingen en aangescherpte regels meestal niet.
Telt het toevoegen van een verplicht veld? Ja. Elke bestaande call laat het weg, dus elke bestaande call faalt nu. Voeg het toe als optioneel met een verstandige standaardwaarde, of versioneer het endpoint.
Telt een bugfix? Kan zijn. Als aanroepers afhankelijk waren van het buggy gedrag, breekt het fixen ervan hen, wat de documentatie ook zei. Behandel elke fix die waarneembare output verandert als breaking, tenzij jullie kunnen aantonen dat niemand erop vertrouwde.
Geldt semantische versionering voor een web-API? De regel wel: breaking changes krijgen een nieuwe majorversie en de oude blijft werken voor een genoemde periode. Het nummer leeft vaak in de URL of een datum-header in plaats van in een pakketversie.
Hoeveel vooraankondiging is genoeg? Genoeg voor een aanroeper om de aankondiging te vinden en het werk te doen. Negentig dagen is een gangbare ondergrens voor publieke API’s; langer voor alles wat wordt gebruikt in code die naar eindgebruikers wordt uitgebracht en niet op afstand kan worden bijgewerkt.
De technische beweringen in dit artikel zijn niet onafhankelijk gecontroleerd. Klopt er iets niet, laat het ons weten, dan corrigeren we het.