Foto av Albert Stoynov via Unsplash
API Versioning: Strategier för bakåtkompatibla API-ändringar
Lär dig hantera API-versioner korrekt med URL-versioning, header-versioning och semantic versioning. Praktiska strategier för att undvika breaking changes.
Varför API-versioning är kritiskt
Ditt API är ett kontrakt med dina konsumenter. Varje ändring som bryter befintliga integrationer kostar tid, pengar och förtroende. API-versioning ger dig en strukturerad metod för att introducera nya funktioner och förbättringar utan att befintliga klienter slutar fungera.
Utan versioning hamnar du i ett av två lägen: antingen kan du aldrig ändra ditt API (vilket leder till teknisk skuld), eller så bryter du klienternas integrationer regelbundet (vilket leder till att ingen vill använda ditt API). Versioning löser detta genom att låta gamla och nya versioner samexistera.
De vanligaste strategierna är URL-versioning, header-versioning och query parameter-versioning. Varje approach har styrkor och svagheter beroende på ditt API:s målgrupp och komplexitet.
// De tre vanligaste versioning-strategierna
// 1. URL-versioning (vanligast, enklast)
GET /api/v1/products
GET /api/v2/products
// 2. Header-versioning (renare URL:er)
GET /api/products
Accept: application/vnd.myapi.v2+json
// 3. Query parameter (enklast att testa)
GET /api/products?version=2URL-versioning i praktiken
URL-versioning är den mest utbredda strategin och den som de flesta utvecklare förväntar sig. Versionnumret inkluderas direkt i URL-sökvägen, vilket gör det omedelbart synligt vilken version som anropas. GitHub, Stripe och Google Maps använder alla denna approach.
Fördelen är enkelhet: varje version kan routas till olika controllers eller till och med olika servrar. Det är trivialt att testa i webbläsaren och kräver inga speciella headers. Nackdelen är att URL:erna tekniskt sett representerar samma resurs, vilket bryter mot REST-principen att en URL ska identifiera en unik resurs.
// Express.js: URL-versioning med separata routers
import express from "express";
import v1Router from "./routes/v1";
import v2Router from "./routes/v2";
const app = express();
// Varje version har sin egen router
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);
// routes/v1/products.ts - Originalformat
router.get("/products", async (req, res) => {
const products = await db.products.findMany();
res.json({
data: products.map((p) => ({
id: p.id,
name: p.name,
price: p.price,
})),
});
});
// routes/v2/products.ts - Nytt format med extra fält
router.get("/products", async (req, res) => {
const products = await db.products.findMany({
include: { category: true, variants: true },
});
res.json({
data: products.map((p) => ({
id: p.id,
name: p.name,
pricing: {
amount: p.price,
currency: p.currency,
formatted: `${p.price} ${p.currency}`,
},
category: p.category.name,
variantCount: p.variants.length,
})),
meta: { version: "v2", count: products.length },
});
});Header-versioning och content negotiation
Header-versioning använder Accept-headern eller en custom header för att specificera vilken version klienten vill ha. URL:en förblir densamma, vilket ger renare API-design. GitHub:s API använder denna approach med Accept: application/vnd.github.v3+json.
Denna strategi passar bra för API:er med en tekniskt kunnig målgrupp som redan konfigurerar headers i sina HTTP-klienter. Den är mindre lämpad för API:er som anropas direkt från webbläsaren eller behöver vara enkla att testa utan verktyg som curl eller Postman.
Ett mellanting är att stödja båda: URL-versioning som primär metod och header-override för avancerade användare.
// Header-versioning middleware
function versionMiddleware(req, res, next) {
// Prioritetsordning: custom header > accept header > default
const customVersion = req.headers["x-api-version"];
const acceptHeader = req.headers.accept || "";
// Parsa version från Accept header
// Format: application/vnd.myapi.v2+json
const acceptMatch = acceptHeader.match(
/application\/vnd\.myapi\.v(\d+)\+json/
);
if (customVersion) {
req.apiVersion = parseInt(customVersion, 10);
} else if (acceptMatch) {
req.apiVersion = parseInt(acceptMatch[1], 10);
} else {
req.apiVersion = 2; // Senaste stabila version
}
// Avvisa okända versioner
const SUPPORTED_VERSIONS = [1, 2, 3];
if (!SUPPORTED_VERSIONS.includes(req.apiVersion)) {
return res.status(400).json({
error: "unsupported_version",
message: `API version ${req.apiVersion} stöds inte`,
supported: SUPPORTED_VERSIONS,
});
}
// Varna för deprecated versioner
if (req.apiVersion === 1) {
res.setHeader("Deprecation", "true");
res.setHeader("Sunset", "2026-09-01");
res.setHeader(
"Link",
'</api/v2/docs>; rel="successor-version"'
);
}
next();
}
// Använd i route handler
router.get("/products", versionMiddleware, async (req, res) => {
if (req.apiVersion === 1) {
return respondV1(req, res);
}
return respondV2(req, res);
});Breaking changes och deprecation
Inte alla ändringar kräver en ny version. Att lägga till ett nytt fält i ett response, lägga till en valfri query parameter eller skapa en ny endpoint är icke-brytande ändringar som kan göras inom samma version.
Brytande ändringar inkluderar: ta bort eller byta namn på ett fält, ändra datatypen på ett fält, ändra URL-strukturen, ändra felformat eller göra en tidigare valfri parameter obligatorisk. Dessa kräver alltid en ny version.
Vid deprecation av en gammal version: kommunicera tidigt, ge minst 6-12 månader, returnera Deprecation och Sunset-headers, och logga vilka klienter som fortfarande använder den gamla versionen så att du kan kontakta dem direkt.
// Deprecation-strategi med automatisk notifiering
interface VersionConfig {
version: number;
status: "current" | "deprecated" | "retired";
sunsetDate?: Date;
}
const VERSION_CONFIG: VersionConfig[] = [
{ version: 1, status: "deprecated", sunsetDate: new Date("2026-09-01") },
{ version: 2, status: "current" },
{ version: 3, status: "current" }, // Beta
];
// Middleware som loggar deprecated version-anrop
async function deprecationTracker(req, res, next) {
const config = VERSION_CONFIG.find(
(v) => v.version === req.apiVersion
);
if (config?.status === "retired") {
return res.status(410).json({
error: "version_retired",
message: `API v${req.apiVersion} är nedlagd`,
migration: `Se /docs/migration/v${req.apiVersion + 1}`,
});
}
if (config?.status === "deprecated") {
// Logga för uppföljning
await db.deprecationLog.create({
data: {
apiKey: req.apiKeyRecord?.prefix || "unknown",
version: req.apiVersion,
endpoint: req.path,
timestamp: new Date(),
},
});
}
next();
}
// Veckovis rapport: vilka klienter använder deprecated versioner?
async function generateMigrationReport() {
const usage = await db.deprecationLog.groupBy({
by: ["apiKey"],
_count: { apiKey: true },
where: {
timestamp: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
},
orderBy: { _count: { apiKey: "desc" } },
});
return usage.map((u) => ({
client: u.apiKey,
requestsLastWeek: u._count.apiKey,
}));
}Praktiska tips och rekommendationer
Välj URL-versioning om ditt API riktar sig till en bred publik eller om enkelhet är prioriterat. Det är den strategi som kräver minst dokumentation och som flest utvecklare känner igen. Stripe, Twilio och de flesta SaaS-API:er använder den.
Dokumentera varje version separat och tydligt. En gemensam changelog som listar vad som ändrats mellan versioner hjälper utvecklare att planera migrering. Publicera migreringsguider med kodexempel som visar exakt vad som behöver ändras.
Kör aldrig fler än två-tre aktiva versioner samtidigt. Varje version du underhåller är en kostnad: bugfixar och säkerhetsuppdateringar behöver backportas. Var tydlig med din deprecation-policy från start.
Använd feature flags internt om du behöver testa nya API-beteenden innan en ny version lanseras. Kombinera med canary releases där en liten andel av trafiken dirigeras till den nya versionen. Övervaka felfrekvens och svarstider noggrant innan fullständig utrullning.
För API:er som hanterar stora datamängder, överväg att kombinera versioning med vår guide om REST API design best practices för att säkerställa att varje version följer konsekventa designmönster.