Hvert team jeg har jobbet i finner til slutt opp sitt eget API-svarsformat. Det virker harmløst først — et lite wrapper-objekt her, en custom feilform der — og seks måneder senere skriver du en fjerde versjon av feil-parsing-middlewaren din og krangler i code review om data.user eller data.result.user er den "riktige" stien. Det finnes ingen universell standard som løser alt dette, men det finnes mønstre som holder i produksjon og antimønstre som helt sikkert kommer tilbake og biter deg. Her er hva jeg faktisk ville satt inn i et design-dokument.

Konsistente suksess-svar

Første spørsmål hvert team debatterer: skal hvert svar pakkes inn i en konvolutt som {"status": "ok", "data": {...}}? Det ærlige svaret er — sannsynligvis ikke som standard. Konvolutter ga mer mening på begynnelsen av 2000-tallet da HTTP-statuskoder ikke alltid var pålitelige gjennom proxyer og mobilnettverk. I dag er et flatt svar som lar ressursen snakke for seg selv, nesten alltid renere. Reserver konvolutten til endepunkter som genuint returnerer blandede payloads, som en bulk-operasjon som delvis lykkes.

json
// ✅ Good — flat, direct, the order IS the response
// GET /v1/orders/ord_9kZ2m
{
  "id": "ord_9kZ2m",
  "status": "fulfilled",
  "customer_id": "cus_4xA1p",
  "total_amount": 149.99,
  "currency": "USD",
  "created_at": "2026-03-15T11:42:00Z",
  "line_items": [
    { "sku": "HDPHN-BLK-XM5", "quantity": 1, "unit_price": 149.99 }
  ]
}

// ❌ Avoid — unnecessary envelope adds a layer clients have to unwrap every time
{
  "status": "success",
  "code": 200,
  "data": {
    "order": {
      "id": "ord_9kZ2m"
    }
  }
}

Innpakking gir mening når du må samle metadata som ikke er en del av selve ressursen — pagineringskursorer, request-ID-er for tracing, eller delvis-feil-oppsummeringer i bulk-endepunkter. For et enkelt GET /orders/:id er ordren svaret. Tving ikke klienter til å skrive response.data.order.id når response.id fungerer fint. Hvis du vil ha en spec å referere til, er JSON:API en meningsfull men gjennomtenkt standard som definerer akkurat når og hvordan man bruker konvolutter — verdt å lese selv om du ikke adopterer den helt.

Feilsvar — bruk RFC 7807 Problem Details

Custom feilformer er en av de vanligste kildene til integrasjonssmerte. Hvert API ender med noe litt forskjellig — {"error": "..."}, {"message": "...", "code": 42}, {"errors": [...]} — og hver klient som konsumerer API-et ditt må skrive skreddersydd feilparsings-logikk. IETF løste dette med RFC 7807 — Problem Details for HTTP APIs. Det er en lettvektsstandard som definerer en konsistent JSON-struktur for feil, med Content-Type application/problem+json. Adopter den, og feilformatet ditt blir noe enhver utvikler kan lese uten å ty til docs.

json
// POST /v1/orders — 422 Unprocessable Entity
// Content-Type: application/problem+json
{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The order could not be created because one or more fields are invalid.",
  "instance": "/v1/orders/requests/req_7bN3k",
  "errors": [
    {
      "field": "line_items[0].quantity",
      "message": "Quantity must be a positive integer."
    },
    {
      "field": "shipping_address.postal_code",
      "message": "Postal code is required for US shipments."
    }
  ]
}
  • Forutsigbar parsing: Klienter vet alltid hvor de finner den menneskelig lesbare meldingen (detail), den maskinlesbare kategorien (type), og HTTP-statusen speilet i bodyen (status).
  • Utvidbar fra designet: Spesifikasjonen tillater eksplisitt ekstra felt som errors for validering på feltnivå — du jobber ikke rundt den.
  • Verktøystøtte: OpenAPI 3.x støtter application/problem+json som response content type, så genererte docs og klient-SDK-er forstår formen nativt.
  • type-URI-en er et dokument, ikke bare en streng: Pek den mot en ekte side som forklarer feilen, og du har akkurat erstattet en supportsak med et selvbetjeningssvar.

HTTP-statuskoder + JSON-body sammen

Statuskoden og JSON-bodyen er ikke overflødige — de spiller forskjellige roller. Statuskoden forteller HTTP-laget (proxyer, cacher, nettlesere, monitoreringsverktøy) hva som skjedde. JSON-bodyen forteller applikasjonslaget ditt. Begge må være riktige. MDNs HTTP-statusreferanse er den raskeste måten å løse debatter om hvilken kode som passer. De som oftest snubler team er 400 vs 422 (begge er klientfeil, men 422 betyr spesifikt at syntaksen var gyldig og serveren forsto den — semantikken var feil), og 401 vs 403 (401 betyr "hvem er du?", 403 betyr "jeg vet hvem du er — du kan ikke gjøre dette").

json
// 400 Bad Request — malformed JSON or missing required field at the HTTP level
{
  "type": "https://api.example.com/problems/bad-request",
  "title": "Bad Request",
  "status": 400,
  "detail": "Request body is not valid JSON."
}

// 422 Unprocessable Entity — valid JSON, but business rules rejected it
{
  "type": "https://api.example.com/problems/insufficient-inventory",
  "title": "Insufficient Inventory",
  "status": 422,
  "detail": "HDPHN-BLK-XM5 has 0 units available; requested 2.",
  "instance": "/v1/orders/requests/req_7bN3k"
}

// 404 Not Found — resource doesn't exist (or you don't want to reveal it does)
{
  "type": "https://api.example.com/problems/not-found",
  "title": "Order Not Found",
  "status": 404,
  "detail": "No order with ID ord_XXXXX exists in this account."
}
  • 200 OK — vellykket GET, PUT, PATCH som returnerer en body
  • 201 Created — vellykket POST som opprettet en ressurs; inkluder en Location-header som peker på den nye ressursen
  • 204 No Content — vellykket DELETE eller handling uten response body; ingen JSON nødvendig
  • 400 Bad Request — ugyldig request-syntaks, serveren kan ikke engang parse den
  • 401 Unauthorized — manglende eller ugyldige autentiseringsdetaljer
  • 403 Forbidden — autentisert, men ikke tillatt
  • 404 Not Found — ressursen finnes ikke
  • 409 Conflict — tilstandskonflikt (f.eks. duplisert ordre, optimistic lock-feil)
  • 422 Unprocessable Entity — gyldig syntaks, feilet på semantisk/forretningsvalidering
  • 429 Too Many Requests — rate limit truffet; inkluder alltid en Retry-After-header
  • 500 Internal Server Error — noe gikk i stykker på serversiden; lekk aldri stack traces i bodyen

Datoer og tider — alltid ISO 8601

Unix-timestamps ser rene ut — bare et tall. Men de er en felle. Er 1710499200 sekunder eller millisekunder? (Begge er vanlige. JavaScripts Date.now() gir millisekunder, POSIX gir sekunder.) Hvilken tidssone? De er uleselige i logger uten en konverterer. De kan ikke representere datoer før 1970 pent. Og de kommer til å overflow 32-bit integers i 2038 på systemer som ikke har migrert ennå. ISO 8601-strenger løser alt dette. Bruk UTC og inkluder alltid tidssoneforskyvningen — en naken 2026-03-15T11:42:00 uten en etterfølgende Z eller +00:00 er tvetydig og vil til slutt forårsake en bug i en klient som antar lokal tid.

json
// ✅ Good — unambiguous, human-readable, timezone-explicit
{
  "created_at": "2026-03-15T11:42:00Z",
  "updated_at": "2026-04-01T08:15:33Z",
  "scheduled_delivery": "2026-03-18T00:00:00Z",
  "expires_at": "2026-04-15T23:59:59Z"
}

// ❌ Avoid — ambiguous, unreadable, seconds vs ms confusion
{
  "created_at": 1710499200,
  "updated_at": 1743494133000,
  "scheduled_delivery": "15/03/2026",
  "expires_at": "April 15, 2026"
}

Null vs utelatte felt

Disse to er ikke det samme, og å blande dem skaper subtile bugs som bare dukker opp i edge-caser. Null betyr at feltet eksisterer, serveren vet om det, og dets nåværende verdi er "ingenting" — som et fulfilled_at-timestamp på en ordre som ikke er sendt ennå. Å utelate et felt helt betyr at det ikke er aktuelt i denne konteksten — som et return_tracking_number på en ikke-returnert ordre. Hvis en klient ser "fulfilled_at": null, vet den at feltet er en del av denne ressursens skjema og er eksplisitt ikke satt. Hvis feltet er fraværende, bør klienten behandle det som utenfor omfanget av dette svaret — noe som betyr noe når du gjør delvise oppdateringer med PATCH. Å sende null betyr "fjern dette feltet"; å utelate det betyr "ikke rør det".

json
// Order that exists but hasn't shipped yet
// fulfilled_at: null — we know about this field, it's just not set yet
// return_tracking_number: omitted — returns don't apply to this order
{
  "id": "ord_9kZ2m",
  "status": "processing",
  "created_at": "2026-03-15T11:42:00Z",
  "fulfilled_at": null,
  "shipped_at": null,
  "tracking_number": null,
  "total_amount": 149.99
}

// PATCH /v1/orders/ord_9kZ2m — cancel the scheduled delivery
// Only include fields you want to change
{
  "scheduled_delivery": null,
  "status": "cancelled"
}
// "total_amount" is omitted — we're NOT zeroing it out, just not touching it

Paginering — kursor fremfor offset

Offset-paginering (?page=3&per_page=20) er intuitiv å implementere og lett å forklare, men den går i stykker stille på levende data. Hvis en post settes inn mens en klient paginerer — mellom side 2 og side 3 — hopper de over et element. Hvis en post slettes, ser de en duplikat. For ethvert datasett som endres ofte (ordrer, events, varslinger), er kursorbasert paginering den riktige standarden. Du gir klienten en ugjennomsiktig kursor (typisk en base64-kodet ID eller timestamp) som representerer posisjonen deres i resultatsettet. Neste side starter fra akkurat det punktet, uavhengig av inserts eller deletes. Offset- paginering er fin for admin-UI-er der datasettet er stabilt, og brukere genuint må hoppe til side 47. Den er ikke fin for noen mobil klient som gjør infinite scroll.

json
// GET /v1/orders?limit=20&cursor=eyJpZCI6Im9yZF85a1oybSJ9
{
  "orders": [
    { "id": "ord_9kZ2m", "status": "fulfilled", "total_amount": 149.99, "created_at": "2026-03-15T11:42:00Z" },
    { "id": "ord_8jY1l", "status": "processing", "total_amount": 89.00, "created_at": "2026-03-14T09:10:00Z" }
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6Im9yZF84alk1bCJ9",
    "has_more": true,
    "limit": 20
  }
}

// When has_more is false, omit next_cursor entirely (or set to null)
// Clients: fetch next page with ?cursor=<next_cursor> until has_more === false

Feltnavngivning — snake_case vs camelCase

Velg én konvensjon og håndhev den med en linter. Selve valget betyr mindre enn konsistensen. Når det er sagt: hvis dine primære konsumenter er JavaScript/TypeScript-klienter, integreres camelCase rent med destructuring og object spread. Hvis dine primære konsumenter er Python- eller Ruby-backender, føles snake_case naturlig. Hvis du betjener begge, er den pragmatiske løsningen å dokumentere konvensjonen og la klienter bruke et transformasjonslag — JSON.parse med en reviver, et Python-humps-bibliotek, eller én enkelt serialiseringskonfigurasjon i frameworket ditt. Det du aldri bør gjøre, er å blande konvensjoner i samme API — customerId ved siden av order_total er et tegn på at forskjellige ingeniører skrev forskjellige endepunkter uten å snakke med hverandre. Bruk JSON Schema Generator for å dokumentere feltnavnene dine konsistent på tvers av endepunkter.

Designtips: Når du designer eller debugger et ekte API-svar, lim inn JSON-en i JSON Formatter — den pretty-printer minifiserte svar, fremhever struktur, og lar deg se navngivningsinkonsistenser ved et raskt blikk før de kommer inn i et klient-SDK.

Versjonering

To skoler: URL-versjonering (/v1/orders, /v2/orders) og header-versjonering (Accept: application/vnd.example.v2+json eller en custom API-Version: 2026-03-15- header). URL-versjonering vinner i praksis nesten hver gang. Den er synlig i logger uten å parse headere, den fungerer med enhver HTTP-klient uten konfigurasjon, du kan teste den i en nettleser, og du kan kjøre v1 og v2 side om side i samme gateway med en enkel path-prefix-regel. Header-versjonering er teoretisk mer RESTfull i henhold til IANA media type- modellen, men det skaper usynlig kompleksitet — en request som ser identisk ut i URL-en oppfører seg faktisk annerledes avhengig av en header de fleste utviklere ikke sjekker først. Stripes datobaserte versjonering (Stripe-Version: 2024-06-20) er det beste fra begge verdener for store plattformer, men det er et annet problem enn å velge ditt første versjonsskjema. Uansett hva du velger, versjonér fra dag én. Å ettermontere versjonering på et uversjonert API i produksjon er smertefullt og går sjelden rent. Bruk JSON Validator for å bekrefte at svarene fra begge API-versjoner er strukturelt forsvarlige under migrasjonstesting.

Oppsummering

Ingenting av dette er banebrytende — men det er poenget. Teamene som sliter mest med API- design, er ikke de som tok teknisk feile valg. De er de som tok forskjellige valg i forskjellige endepunkter og aldri skrev dem ned. Flate suksess-svar. RFC 7807-feil-bodies. ISO 8601- datoer. Kursorpaginering på levende data. Null for "kjent og tom", utelatt for "gjelder ikke". URL- versjonering fra dag én. Disse mønstrene er ikke perfekte, men de er forutsigbare — og forutsigbarhet er det som gjør et API til en glede å integrere med i stedet for et puslespill å reverse-engineere. Den formelle JSON- spesifikasjonen bor på RFC 8259 hvis du noen gang må avgjøre et argument på spec-nivå. For alt over det laget er den beste standarden den teamet ditt faktisk skriver ned og følger konsekvent.