Hvordan du undgår det kaos-API jeg selv startede med
De fleste tror et API bare er nogle endpoints. Det passer ikke.
Jeg troede helt ærligt, at et REST API bare var: lav nogle URLs, smid lidt JSON ud, done. Første gang jeg selv byggede et API til et lille hobby-dashboard, opfandt jeg nye patterns for hver eneste route. Nogle steder hed det /users, andre /user-list, et enkelt sted /getUsers (jeg skammer mig stadig lidt).
Det virkede. Lige indtil jeg ville bygge en ordentlig frontend ovenpå. Pludselig var alt skrøbeligt. Ingen konsistens, pagination var hjemmerullet på tre forskellige måder, og fejlbeskederne var en blanding af engelske tekster, danske tekster og rene stacktraces.
Pointen: godt REST API-design handler mindre om store enterprise-ord og mere om at lave en stabil kontrakt mellem din backend og din frontend. Selv i små projekter. Især i små projekter, faktisk, fordi du ofte er både backend- og frontend-person på én gang.
I stedet for at prøve at dække hele REST-teorien vil jeg vise dig et lille sæt tommelfingerregler, du kan bruge som dit eget “API-kontrakt-kit”. Et fast mønster for:
Standard-endpoints. Pagination. Filtering og sorting. Fejlformat. Statuskoder. Versioning-light. Og lidt rate limiting uden at det bliver sikkerheds-afhandling.
Hvordan et “pænt” API føles når du bruger det
Lad mig starte fra den side, der gør mest ondt: at være klienten. Forestil dig, at du bygger en lille frontend, der skal bruge data fra dit eget API. Du skriver kode som:
const res = await fetch("/api/users?page=2&pageSize=20");
const data = await res.json();
Hvis API’et er godt designet, er det her, du ikke behøver tænke så meget. Du forventer nogenlunde det her:
{
"data": [
{ "id": 1, "name": "Ida" },
{ "id": 2, "name": "Sara" }
],
"page": 2,
"pageSize": 20,
"total": 57
}
Og når noget går galt, forventer du ikke en hel HTML-fejlside smidt i hovedet. Du forventer noget, du kan bruge direkte i UI’et:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": {
"email": "Email is not valid"
}
}
}
Når dit API føles sådan at bruge, bliver frontend-koden meget roligere. Færre specielle tilfælde. Færre if (res.status === 418)-agtige hacks.
Så hvordan designer du det, når du selv sidder og bygger din første backend i f.eks. Node/Express, Flask eller Django? Det er det, resten af artiklen handler om.
Hvordan dine standard-endpoints bliver forudsigelige
Et godt startsted er at gøre dine endpoints så kedeligt forudsigelige som muligt. Ikke kreative. Ikke “sjove”. Bare det samme mønster hver gang.
Jeg bruger typisk de her fem typer til et givent resource-navn (lad os sige users):
Liste:
GET /api/users
Returnerer en pagineret liste af brugere.
Get by id:
GET /api/users/:id
Returnerer én bruger eller 404.
Create:
POST /api/users
Body indeholder data til at oprette en ny bruger. Returnerer 201 + den nye resource.
Update (helt eller delvist):
PATCH /api/users/:id
Body indeholder felter, der skal opdateres. Returnerer 200 + den opdaterede resource.
Slet:
DELETE /api/users/:id
Returnerer typisk 204 (no content), hvis sletning lykkes.
Bemærk hvad jeg ikke gør: Jeg skriver ikke /getUsers, /createUser, /deleteUserById. Verbet ligger i HTTP-metoden (GET, POST, PATCH, DELETE), navneordet ligger i URL’en.
Det gør en kæmpe forskel, når du får flere resourcer. Pludselig har du et helt API, der ligner sig selv:
GET /api/users
GET /api/users/:id
POST /api/users
PATCH /api/users/:id
DELETE /api/users/:id
GET /api/posts
GET /api/posts/:id
POST /api/posts
PATCH /api/posts/:id
DELETE /api/posts/:id
Hvis du vil se et nogenlunde rent REST-agtigt API i praksis, kan du fx kigge på, hvordan mange små backend-øvelser er bygget op på Coding Class. Mønstret går igen, fordi det simpelthen er nemmere at arbejde med.
Hvordan du vælger pagination uden at fortryde senere
Pagination er et af de steder, hvor jeg selv fik mest rod i starten. Nogle endpoints havde ?page=, nogle havde ?offset=, og ét sted var der et helt hjemmelavet system med nextPageToken, jeg ikke engang selv kunne huske bagefter.
Der er to klassiske måder at paginere på:
Offset pagination: enkel og fin til små projekter
Offset-pagination er den nemme model, du ofte ser med SQL: LIMIT og OFFSET. Du eksponerer det som typisk to query-parametre:
GET /api/users?offset=0&limit=20
GET /api/users?offset=20&limit=20
Respons kan se sådan her ud:
{
"data": [/* 20 brugere */],
"pagination": {
"offset": 20,
"limit": 20,
"total": 57
}
}
Fordele:
- Nemt at implementere i de fleste databaser.
- Nemt at forstå i frontend.
- Super fint til små projekter og hobby-ting.
Ulemperne (som du typisk ikke mærker, før du har meget data): det kan blive langsomt ved store tabeller, og hvis data ændrer sig meget imens du paginerer, kan du se lidt “hop” i resultaterne. Men hvis du er på begynderniveau og bygger mindre ting, er offset helt ok.
Cursor pagination: til når du har brug for stabilitet
Cursor-baseret pagination bruger en slags “markør”, der peger på, hvor du er nået til. Typisk et id eller et tidsstempel. URL’en kan se sådan her ud:
GET /api/users?limit=20
GET /api/users?limit=20&after=cku83kdiq0001x9s6iz4
Respons:
{
"data": [/* 20 brugere */],
"pagination": {
"limit": 20,
"nextCursor": "cku83l8z80003x9s6cvj"
}
}
Frontend gemmer nextCursor og bruger den til at hente næste side. Det er mere stabilt ift. nye rækker og sletninger, men kræver lige et ekstra mentalt skridt.
Mit råd, hvis du er i gang med dit første seriøse API: start med offset. Men gør det konsekvent, og pak metadata ind under et pagination-felt, så du kan skifte strategi senere uden at knække alt.
Et simpelt Node/Express-offset-eksempel kunne se sådan her ud:
app.get("/api/users", async (req, res) => {
const limit = Math.min(Number(req.query.limit) || 20, 100);
const offset = Number(req.query.offset) || 0;
const [rows, total] = await getUsersFromDb({ limit, offset });
res.json({
data: rows,
pagination: {
limit,
offset,
total
}
});
});
Hvis du en dag vil skifte til cursor, kan du beholde strukturen { data, pagination } og bare ændre indholdet af pagination. Frontend behøver ikke omskrives fuldstændigt.
Hvordan dine query params til filtering og sorting bliver logiske
Nu kommer vi til den del, hvor API’er ofte bliver rodede: filtering og sorting. Jeg har selv lavet endpoints med ?searchText= ét sted, ?q= et andet og ?filter_name= et tredje. Det er sjovt i kodetid, knap så sjovt tre måneder senere.
Hvis du vælger nogle simple navngivningsregler nu, bliver du virkelig glad for dig selv senere.
Enkle regler for filtering
Jeg bruger typisk dette mønster:
- Ren tekst-søgning:
q(kort og kendt navn). - Filter på specifikke felter: bare brug feltnavnene.
- Flere værdier: kommasepareret eller gentagne parametre.
Eksempler:
GET /api/users?q=ida
GET /api/users?role=admin
GET /api/users?role=admin,user
GET /api/users?status=active&country=dk
Eller hvis du foretrækker gentagne parametre:
GET /api/users?role=admin&role=user
Vigtigt: vælg én stil og hold dig til den i hele API’et. Så ved din frontend-hjerne altid, hvad den kan forvente.
Sorting uden magi
Sorting kan du gøre ret simpelt med to parametre:
sortBy– hvilket felt der sorteres efter.sortOrder– retning, typiskascellerdesc.
Eksempler:
GET /api/users?sortBy=createdAt&sortOrder=desc
GET /api/users?sortBy=name&sortOrder=asc
Du kan også pakke det i en enkelt streng, hvis du vil, fx sort=-createdAt for descending, men det kræver mere parsing. Til begynderniveau er to parametre fint.
I backend kan du have en lille whitelist, så folk ikke kan sortere på hvad som helst:
const ALLOWED_SORT_FIELDS = ["createdAt", "name", "email"];
function getSortFromQuery(query) {
const sortBy = ALLOWED_SORT_FIELDS.includes(query.sortBy)
? query.sortBy
: "createdAt";
const sortOrder = query.sortOrder === "asc" ? "asc" : "desc";
return { sortBy, sortOrder };
}
Så slipper du for, at en eller anden (måske dig selv) prøver at sortere på et felt, der ikke findes, og får en mærkelig SQL-fejl midt i det hele.
Hvordan du designer et fejlformat, der er til at arbejde med
Mit største irritationsmoment, når jeg bruger andres API’er: hver eneste endpoint har sit eget tilfældige fejlformat. Én route returnerer { error: "Something went wrong" }, en anden returnerer HTML, en tredje smider bare 500 uden body.
Hvis du kun tager én ting med fra den her artikel, så lad det være: vælg ét fejlformat og brug det overalt i dine JSON-responser.
Et simpelt, genbrugeligt fejlformat
Jeg kan godt lide noget i den her stil:
{
"error": {
"code": "SOME_MACHINE_READABLE_CODE",
"message": "En kort menneskelig forklaring",
"details": null
}
}
Hvor:
codeer en kort, maskinvenlig tekst, du kan bruge i frontend til at vise specifikke beskeder.messageer en kort, generel besked, du kan vise direkte til brugeren eller udvikleren.detailser entennulleller et objekt med ekstra info, typisk valideringsfejl.
Eksempler på code kunne være:
NOT_FOUNDUNAUTHORIZEDVALIDATION_ERRORRATE_LIMITEDINTERNAL_SERVER_ERROR
Det behøver ikke være perfekt navngivet fra start. Det vigtige er, at du bruger den samme struktur konsekvent.
Valideringsfejl vs serverfejl
Der er især én type fejl, du bør behandle ordentligt: valideringsfejl på input. Altså når brugeren (eller din frontend) har sendt noget, der ikke giver mening. Her er det virkelig rart at få felt-for-felt-fejl med tilbage.
Jeg bruger typisk noget i stil med:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"details": {
"email": "Email is not valid",
"password": "Password must be at least 8 characters"
}
}
}
Så kan frontend lave noget så simpelt som:
if (res.status === 400 && body.error?.code === "VALIDATION_ERROR") {
const fieldErrors = body.error.details;
// fx vise fieldErrors.email ved email-inputtet
}
Serverfejl (noget brændte sammen i din backend eller database) kan bruge samme format, men uden details:
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "Unexpected error occurred",
"details": null
}
}
Og nej, du behøver ikke vise den fulde stacktrace til klienten. Gem den i logs i stedet.
Hvis du vil se et eksempel på, hvordan man arbejder konsekvent med fejl, kan du overveje at bygge et lille API selv, der returnerer valideringsfejl til en simpel form. På Coding Class ligger der flere øvelser, hvor du netop kan træne det samspil mellem frontend og backend.
Hvordan HTTP status codes bliver dit sprog i stedet for magi
HTTP-statuskoder kan virke som sådan et lidt støvet emne, men det er faktisk bare tal, der hjælper dine klienter med at forstå, hvad der lige skete.
Her er en lille “top 10”, som dækker overraskende meget i små projekter:
- 200 OK – Alt gik godt, her er din data.
- 201 Created – Noget blev oprettet, typisk ved POST.
- 204 No Content – Operation lykkedes, men der er ikke noget body (brugbar til DELETE).
- 400 Bad Request – Klienten sendte noget sludder (fx valideringsfejl).
- 401 Unauthorized – Du mangler at logge ind / token er ugyldig.
- 403 Forbidden – Du er logget ind, men har ikke lov til det her.
- 404 Not Found – Resource findes ikke.
- 409 Conflict – Konflikt, fx du prøver at oprette noget, der allerede findes.
- 429 Too Many Requests – Du rammer rate limit.
- 500 Internal Server Error – Noget gik galt på serveren.
En simpel huskeregel: 2xx betyder “alt ok”, 4xx betyder “klienten gjorde noget forkert”, 5xx betyder “serveren gjorde noget forkert”.
Kombinerer du statuskoderne med det faste fejlformat fra før, får du noget frontend-venligt som:
if (!res.ok) {
const body = await res.json();
if (res.status === 400 && body.error?.code === "VALIDATION_ERROR") {
// vis feltfejl
} else {
// vis generel fejlbesked
}
}
Hvordan du tænker versioning før du faktisk har brug for det
Versionering af API’er kan hurtigt blive en religionskrig. Men hvis du lige nu sidder med dit første eller andet API, er pointen meget mere jordnær:
Du vil gerne undgå at ødelægge ting for eksisterende klienter, når du får en “genial” idé til at ændre strukturen på dit API.
Der er to niveauer:
Niveau 1: Bryd ikke kontrakten unødigt
Det her kan du gøre uden at tænke i /v1, /v2 og alt det der:
- Fjern ikke felter fra dine responses, hvis nogen kunne finde på at bruge dem.
- Skift ikke type på felter (fx fra string til number) uden meget god grund.
- Tilføj gerne nye felter, det er næsten altid ok.
- Lav nye endpoints i stedet for at lave et eksisterende helt om.
Hvis du holder dig til det, kan du faktisk komme ret langt uden eksplicit versionsnummer i din URL.
Niveau 2: Når du rent faktisk har brug for /v2
En dag får du måske brug for at lave noget fundamentalt om: ny auth-model, helt andet responsformat, ny struktur på resourcer. Så giver det mening at lave en ny version.
Det nemmeste sted at starte er at smide versionen i URL’en:
/api/v1/users
/api/v2/users
Du kan også versionere pr. resource, men til små projekter vil jeg hellere holde det simpelt.
Mit råd: vent med at introducere v1, til du /v1 fra dag 1, men til et hobby-API gør det mest bare URL’en længere.
Hvordan du tænker rate limiting uden at blive sikkerhedsekspert
Rate limiting handler i praksis om at sige: “du må ikke spamme mit API uendeligt”. Det beskytter både dig selv og alle andre, der bruger API’et.
Selv i små projekter er det værd at have en eller anden form for begrænsning. Ikke nødvendigvis super avanceret, men nok til at en fejl-loop i frontend ikke DDoS’er dig ved et uheld.
Et simpelt mentalt model for begyndere
Tænk i spande: hver bruger (eller IP, eller API-nøgle) får en lille “spand” af tilladte requests i et givent tidsrum. Hvis spanden er tom, får de et 429-svar indtil der er plads igen.
Eksempel på noget ret forsigtigt:
- Max 60 requests per minut per IP.
- Nogle få “kritiske” endpoints med strammere grænser.
Responsen ved rate limit kan følge samme fejlformat:
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests, please try again later",
"details": {
"retryAfterSeconds": 30
}
}
}
Frontend kan så f.eks. vise en besked eller disable en knap midlertidigt.
Implementeringen afhænger meget af tech-stack, så jeg vil ikke gå dybt ned i det her. Men de fleste populære frameworks har middleware eller biblioteker til det. I Express-verdenen er der fx express-rate-limit, som du kan kombinere med dine egne regler.
Hold det simpelt i starten: beskyt dine mest udsatte endpoints (login, søgning, tunge rapporter) og få 429-svaret på plads. Det alene gør en stor forskel, når noget en dag går galt.
Hvordan du selv kommer i gang uden at drukne
Hvis du lige nu tænker “okay, det var en del”, så er du ikke alene. Da jeg selv begyndte at nørkle med REST, sad jeg med alt for mange blogindlæg åbne og følte, jeg gjorde alting “forkert”.
Mit bedste forslag er at tage det i små bidder og behandle det som et øveprojekt. F.eks. kunne du:
Bygge et lille API til noget, du faktisk gider: måske en liste over dine yndlingsspil, en simpel to-do, et lille bogkartotek. Vælg én resource til at starte med, fx /api/games.
For den resource:
- Lav de fem basis-endpoints (list, get by id, create, update, delete).
- Tilføj offset-pagination på
GET /api/games. - Tilføj filtering på 1-2 felter, fx
?genre=og?q=. - Indfør dit faste fejlformat for både validering og serverfejl.
- Brug meningsfulde statuskoder.
Og vigtigst: byg en lille frontend, der bruger dit API. Det er først der, du opdager, hvor meget forskel et stabilt API-design gør i praksis.
Hvis du får blod på tanden, kan du bygge videre i retning af mere avancerede ting som auth, flere resourcer og måske et rigtigt deploy. Mange af de næste skridt hænger fint sammen med andre emner som debugging, environment variables og deployment, som du også kan finde artikler om på Coding Class.
Jeg er ret overbevist om, at de næste par år bliver sådan en periode, hvor “små” API’er og hobbyprojekter vokser op og skal holde til rigtige brugere. Spørgsmålet er lidt: vil du så sidde med et kaos-API du ikke tør røre, eller et roligt, kedeligt, forudsigeligt et, du faktisk kan bygge videre på?









3 comments