Et API er et løfte. Når en app, en partner eller en anden afdeling bygger på det, bliver hver eneste detalje en del af løftet: feltnavne, datatyper, statuskoder, rækkefølgen af resultater, ja selv de fejl, man glemte at dokumentere. API'er, der holder, er ikke dem, der aldrig ændres, men dem, der kan ændres uden at bryde det, der allerede bygger på dem.
Eksemplerne bruger den fiktive virksomhed Space Travel og dens booking-API, men principperne gælder for de fleste HTTP-API'er.
Kontrakten først
Det mest effektive greb er at skrive kontrakten, før koden findes. Med OpenAPI beskriver man endpoints, parametre, request- og response-formater og fejl i én fil, som både mennesker og værktøjer kan læse. Det giver tre ting på én gang:
- En samtale før koden. Frontend, backend og partnere kan diskutere og ændre kontrakten, mens det stadig er billigt.
- Generering og validering. Klienter, typer og mock-servere kan genereres fra kontrakten, og serveren kan valideres mod den i testene.
- Én sandhed. Dokumentationen er kontrakten, så de to kan ikke glide fra hinanden.
Et uddrag af Space Travels kontrakt for at booke pladser på en rejse:
paths:
/bookings:
post:
operationId: createBooking
summary: Book seats on a trip
parameters:
- name: Idempotency-Key
in: header
required: true
description: Unique per booking attempt. A retry with the same key returns the original result.
schema:
type: string
maxLength: 64
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/BookingRequest"
responses:
"201":
description: The booking was created.
content:
application/json:
schema:
$ref: "#/components/schemas/Booking"
"409":
$ref: "#/components/responses/Problem"
"422":
$ref: "#/components/responses/Problem"
components:
responses:
Problem:
description: An error as Problem Details (RFC 9457).
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Problem"Versionering og dens afvejninger
Før eller siden kommer der en ændring, der ikke kan laves bagudkompatibelt. Der findes tre udbredte måder at versionere på:
- I URL'en (
/v1/bookings). Synlig, nem at route og cache, og nem at forstå for alle. Ulempen er, at hele API'et skifter version på én gang, også de dele, der ikke er ændret. - I en header eller medietype (
Accept: application/vnd.space-travel.v2+json). URL'erne forbliver stabile, og versionen kan vælges pr. ressource. Til gengæld er den usynlig i en browser og lettere at glemme for klienter. - Datobaserede versioner, hvor klienten fastlåser den version, den blev bygget mod, og ændringer udgives som nye datoer. Det giver små, hyppige skridt, men kræver mere disciplin og værktøj på serveren.
Ingen af dem er rigtig i sig selv. Det vigtigste er at vælge én, bruge den konsekvent og skrive ned, hvornår en version udfases. HTTP har standardiserede headere til netop det: Deprecation (RFC 9745) fortæller, at en ressource er eller bliver forældet, og Sunset (RFC 8594) fortæller, hvornår den holder op med at svare.
Bagudkompatibelt eller brydende?
En nyttig tommelfingerregel er, at man gerne må tilføje, men ikke fjerne eller ændre betydning.
- Som regel sikkert: et nyt endpoint, et nyt valgfrit felt i et request, et nyt felt i et response, en ny valgfri query-parameter.
- Brydende: at fjerne eller omdøbe et felt, ændre en datatype, gøre et valgfrit felt påkrævet, ændre en statuskode, eller give et eksisterende felt en ny betydning.
Bemærk "som regel". Et nyt felt i et response bryder klienter, der fejler på ukendte felter, og en ny værdi i en enum bryder klienter, der antager, at listen er udtømmende. Skriv derfor i kontrakten, at klienter skal ignorere ukendte felter og tåle nye enum-værdier, og test jeres egne klienter mod det.
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1798761600
Sunset: Fri, 31 Dec 2027 23:59:59 GMT
Link: <https://api.space-travel.example/v2/bookings>; rel="successor-version"Fejl i et fast format
Fejl er også en del af kontrakten, og de fortjener samme omhu som succes-svarene. RFC 9457, Problem Details for HTTP APIs, beskriver et fælles format med medietypen application/problem+json og felterne type, title, status, detail og instance. Man må tilføje egne felter, der hjælper klienten videre. Når Space Travels rejse er udsolgt, svarer API'et sådan:
{
"type": "https://api.space-travel.example/problems/trip-sold-out",
"title": "The trip is sold out.",
"status": 409,
"detail": "Trip MARS-2031-07 has no free seats left; 2 were requested.",
"instance": "/v1/bookings/attempts/7f3c9a",
"tripId": "MARS-2031-07",
"seatsLeft": 0
}type er en stabil URI, som klienten kan genkende og reagere på, mens title og detail er til mennesker. Klienter skal aldrig parse teksten for at finde ud af, hvad der gik galt.
Paginering
Lister vokser. Et endpoint, der returnerer alt, holder kun, indtil der er for meget. Der er to udbredte mønstre:
- Offset og limit (
?offset=40&limit=20) er simpelt og lader brugeren springe til en bestemt side. Til gengæld kan elementer blive vist to gange eller springes over, når data ændres mellem to forespørgsler, og store offsets bliver langsomme i databasen. - Cursor (
?cursor=…&limit=20, hvor svaret indeholdernextCursor) peger på, hvor den forrige side sluttede. Det er stabilt, mens data ændres, og hurtigt uanset hvor langt man er. Til gengæld kan man ikke springe direkte til side 37.
Uanset valget: sæt en øvre grænse for limit, og hold sorteringen entydig, så to elementer med samme værdi altid kommer i samme rækkefølge.
Idempotens
Netværk svigter. En klient, der sender en booking og ikke får svar, ved ikke, om bookingen blev oprettet. Prøver den igen, risikerer den at booke to gange.
HTTP's metoder GET, PUT og DELETE er idempotente: at gentage dem har samme effekt som at kalde dem én gang. POST er det ikke. Et udbredt mønster er en Idempotency-Key-header, som i kontrakten ovenfor: klienten genererer en unik nøgle pr. forsøg, og serveren gemmer resultatet under nøglen. Kommer samme nøgle igen, returneres det oprindelige resultat i stedet for at oprette en ny booking. Mønsteret er udbredt i betalings-API'er; IETF-kladden, der beskriver headeren, er ikke blevet en standard, så kontrakten bør selv beskrive, hvordan nøglen bruges, og hvor længe den huskes.
Hold løftet over tid
Et API, der holder, er sjældent resultatet af et enkelt godt design. Det er resultatet af en proces: kontrakten skrives og gennemgås før koden, hver ændring vurderes som enten bagudkompatibel eller brydende, brydende ændringer får en ny version og en udfasningsdato, og kontrakten testes, så serveren ikke stille glider væk fra den. Det lyder som meget, men det er billigere end at opdage, at en partners integration er gået i stykker, fordi et felt skiftede navn.
Kort sagt
- Skriv kontrakten i OpenAPI, før koden findes, og lad den være dokumentationen.
- Vælg én versioneringsstrategi, brug den konsekvent, og annoncér udfasning med Deprecation og Sunset.
- Tilføj gerne, men fjern eller ændr aldrig betydning uden en ny version.
- Returnér fejl som Problem Details (RFC 9457) med en stabil type.
- Cursor-paginering med en øvre grænse holder, mens data ændres.
- Gør POST sikker at gentage med en idempotensnøgle, beskrevet i kontrakten.
Skal jeres API holde i mange år?
Vi hjælper gerne med kontrakter, versionering og en udgivelsesproces, der lader API'et udvikle sig uden at bryde dem, der bygger på det. Tag en uforpligtende snak.
- api
- rest
- openapi
- versionering



