EKSING
API Udvikling

API-design der holder: kontrakter, versionering og bagudkompatibilitet

Kontrakten først med OpenAPI, versionering og dens afvejninger, forskellen på bagudkompatible og brydende ændringer, fejl som Problem Details (RFC 9457), paginering og idempotens.

Claus Eksing
15. oktober 2026 · 6 min læsning
Blå netværkskabler forbundet til et serverrack

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:

openapi.yaml (excerpt)
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.

Response from a deprecated v1 endpoint
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:

HTTP 409 · Content-Type: application/problem+json
{
  "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 indeholder nextCursor) 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.

Book en uforpligtende samtale
  • api
  • rest
  • openapi
  • versionering
Claus Eksing
Senior softwarearkitekt med 25+ års erfaring i systemdesign, API-udvikling og AI-understøttet udvikling.
Book en samtale

Relaterede indlæg

Se alle indlæg →
En robothånd og en menneskehånd rækker ud mod hinanden på en turkis baggrund
AI & Vibe Coding

AI-understøttet udvikling uden at gå på kompromis med kvaliteten

Hvad AI-kodeværktøjer og vibe coding er gode til, hvor både agenten og vi mennesker fejler, og de rammer, der giver farten uden at betale med kvalitet og sikkerhed: tests først, små ændringer, menneskelig gennemgang og klare regler for agenten.

23. oktober 2026 · 7 min læsning