Foto av Max Duzij via Unsplash
REST API Design: Best Practices för 2026
Moderna designmönster för REST-API:er som håller i produktion. Versionering, felhantering, HATEOAS och dokumentation med OpenAPI 3.1.
API-design som håller över tid
Ett väldesignat REST-API är som ett kontrakt: det måste vara tydligt, förutsägbart och stabilt. Dålig API-design leder till brytande förändringar, frustrerade konsumenter och teknisk skuld som ackumuleras med varje release.
De senaste årens utveckling har förfinat hur vi tänker kring API:er. Versionering är inte längre valfritt. Felmeddelanden behöver vara maskinläsbara och människovänliga samtidigt. Och dokumentation som inte uppdateras automatiskt är värre än ingen dokumentation alls.
Här går jag igenom de designprinciper som skiljer robusta API:er från sköra prototyper.
URL-design och resursmodellering
Bra URL-design handlar om att spegla din domänmodell, inte din databasstruktur. Resurser ska vara substantiv i plural, hierarkier ska vara logiska och inte djupare än två nivåer.
Använd query-parametrar för filtrering, sortering och paginering — inte för att identifiera resurser. Undvik verb i URL:er: det är HTTP-metoderna som uttrycker operationen.
// Bra URL-design
GET /api/v2/products // Lista produkter
GET /api/v2/products/abc-123 // Hämta en produkt
GET /api/v2/products/abc-123/reviews // Produktens recensioner
POST /api/v2/products // Skapa produkt
// Filtrering och paginering via query-parametrar
GET /api/v2/products?category=electronics&sort=-createdAt&limit=25
// Dålig design — undvik detta
GET /api/v2/getProducts // Verb i URL
GET /api/v2/products/abc-123/reviews/r-456/author/posts // För djup
POST /api/v2/products/delete/abc-123 // Använd DELETE iställetKonsekvent felhantering med RFC 9457
Felmeddelanden är en del av ditt API:s kontrakt. RFC 9457 (tidigare RFC 7807) definierar Problem Details — ett standardformat för felresponser som alla konsumenter kan parsa programmatiskt.
Fördelen med Problem Details är att format och struktur är identiska oavsett feltyp. Klienten behöver inte gissa hur olika fel representeras. Lägg till egna fält vid behov, men behåll alltid grundstrukturen.
// Problem Details (RFC 9457)
import { Request, Response, NextFunction } from "express";
interface ProblemDetail {
type: string;
title: string;
status: number;
detail: string;
instance?: string;
[key: string]: unknown;
}
class ApiError extends Error {
constructor(
public status: number,
public title: string,
public detail: string,
public type: string = "about:blank",
public extensions: Record<string, unknown> = {}
) {
super(detail);
}
}
function errorHandler(
err: Error, req: Request,
res: Response, _next: NextFunction
) {
if (err instanceof ApiError) {
const problem: ProblemDetail = {
type: err.type,
title: err.title,
status: err.status,
detail: err.detail,
instance: req.originalUrl,
...err.extensions,
};
return res.status(err.status)
.type("application/problem+json")
.json(problem);
}
res.status(500).type("application/problem+json").json({
type: "about:blank",
title: "Internt serverfel",
status: 500,
detail: "Ett oväntat fel inträffade",
instance: req.originalUrl,
});
}Versionering utan kaos
API-versionering är oundvikligt — frågan är hur du gör det utan att bryta befintliga konsumenters integrationer. Det finns tre vanliga strategier: URL-prefix (/v2/), header-baserad (Accept: application/vnd.api+json;version=2) och query-parameter (?version=2).
URL-prefix är enklast att implementera och mest transparent. Header-baserad versionering ger renare URL:er men är svårare att testa i webbläsaren. Min rekommendation: välj URL-prefix om du inte har starka skäl att göra annorlunda.
Viktigast är att ha en deprecation-strategi. Kommunicera tydligt med Sunset- och Deprecation-headers när äldre versioner fasas ut.
// Versionshantering med sunset-headers
import express from "express";
const v1Router = express.Router();
const v2Router = express.Router();
// v1 — deprecated, stängs 2026-09-01
v1Router.use((req, res, next) => {
res.set("Deprecation", "true");
res.set("Sunset", "Sat, 01 Sep 2026 00:00:00 GMT");
res.set("Link",
'</api/v2' + req.path + '>; rel="successor-version"'
);
next();
});
v1Router.get("/products", async (req, res) => {
const products = await db.products.findMany();
res.json(products); // Flat struktur i v1
});
// v2 — wrapper med paginering
v2Router.get("/products", async (req, res) => {
const { cursor, limit = 20 } = req.query;
const products = await db.products.findMany({
take: Number(limit) + 1,
cursor: cursor ? { id: String(cursor) } : undefined,
});
const hasMore = products.length > Number(limit);
const data = hasMore ? products.slice(0, -1) : products;
res.json({
data,
meta: {
hasMore,
nextCursor: hasMore
? data[data.length - 1].id : null,
},
});
});
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);OpenAPI 3.1 och automatiserad dokumentation
Dokumentation som skrivs separat från koden blir snabbt inaktuell. OpenAPI 3.1 (med full JSON Schema-kompatibilitet) är standarden för att beskriva REST-API:er maskinläsbart.
Använd ett code-first-approach: generera OpenAPI-specifikationen från din kod med bibliotek som tsoa, zod-openapi eller fastify med @fastify/swagger. Specifikationen driver sedan Swagger UI, klientgenerering och automatiserade kontraktstester.
Vanligt misstag: att skriva OpenAPI-specifikationen för hand i en YAML-fil som aldrig uppdateras. Code-first eliminerar det problemet helt.
// Code-first OpenAPI med Zod
import { z } from "zod";
import { extendZodWithOpenApi } from "zod-openapi";
extendZodWithOpenApi(z);
const ProductSchema = z.object({
id: z.string().uuid().openapi({
description: "Unikt produkt-ID",
}),
name: z.string().min(1).max(200).openapi({
description: "Produktnamn",
example: "Trådlös mus",
}),
price: z.number().positive().openapi({
description: "Pris i SEK",
example: 299,
}),
category: z.enum([
"electronics", "clothing", "food"
]).openapi({
description: "Produktkategori",
}),
createdAt: z.string().datetime().openapi({
description: "ISO 8601 tidsstämpel",
}),
});
// Schemat genererar automatiskt
// OpenAPI-dokumentation som alltid
// matchar din faktiska implementation