Introduktion
“Hvordan bør vi versionere vores API?” er et spørgsmål, der udløser overraskende ophedede debatter. Efter at have bygget og vedligeholdt API’er brugt af tusindvis af udviklere har jeg lært, at den bedste tilgang afhænger af konteksten - men nogle strategier er klart bedre end andre.
Hvorfor versionere API’er?
API’er er kontrakter. Når du ændrer en API, risikerer du at ødelægge clients, der er afhængige af den gamle adfærd. Versionering lader dig:
- Udvikle din API uden at ødelægge eksisterende clients
- Udfase gammel funktionalitet på en ordentlig måde
- Understøtte flere client-versioner samtidigt
De vigtigste tilgange
URL-sti-versionering
GET /v1/users/123
GET /v2/users/123
Fordele:
- Ekstremt klar og synlig
- Nem at route på load balancer-niveau
- Enkel at implementere
- Nem at dokumentere
Ulemper:
- Fylder URL’er op
- Kan føre til kodeduplikering
- Fristende at oprette for mange versioner
Hvornår bruges det: Offentlige API’er, API’er med mange eksterne forbrugere, når maksimal klarhed ønskes.
Versionering via query-parameter
GET /users/123?version=1
GET /users/123?api-version=2023-01-15
Fordele:
- Holder URL’er rene
- Valgfri parameter (kan som standard bruge nyeste version)
- Nem at tilføje til eksisterende API’er
Ulemper:
- Let at glemme
- Kan caches forkert
- Mindre synlig i logs og dokumentation
Hvornår bruges det: Interne API’er, når versionering skal være valgfri.
Header-versionering
GET /users/123
Accept: application/vnd.myapi.v1+json
GET /users/123
X-API-Version: 2
Fordele:
- Rene URL’er
- Følger HTTP-semantik (content negotiation)
- Adskiller versionering fra ressourceidentifikation
Ulemper:
- Skjult ved en hurtig gennemgang
- Sværere at teste i browseren
- Mere kompleks client-implementering
Hvornår bruges det: Når REST-renhed er vigtig, hos sofistikerede API-forbrugere.
Datobaseret versionering
GET /users/123
Stripe-Version: 2023-10-16
Fordele:
- Klar tidslinje over ændringer
- Fremmer inkrementel udvikling
- Ingen vilkårlige versionsnumre
Ulemper:
- Kræver at holde styr på, hvad der ændrede sig hvornår
- Kan være forvirrende (hvilken dato skal jeg bruge?)
- Sværere at kommunikere store ændringer
Hvornår bruges det: API’er, der udvikler sig hyppigt med små ændringer (Stripes tilgang).
Min anbefaling
Til de fleste API’er anbefaler jeg URL-sti-versionering med et twist:
Strategien
-
Brug URL-versionering kun til hovedversioner
/v1/users /v2/users # Kun når breaking changes er uundgåelige -
Udvikl inden for versioner ved hjælp af additive ændringer
- Tilføj nye felter (fjern ikke gamle)
- Tilføj nye endpoints
- Tilføj nye valgfrie parametre
-
Brug feature flags til gradvise rollouts
GET /v1/users/123?include=new_profile_fields
Hvorfor det virker
- Klarhed: Udviklere ser med det samme, hvilken version de bruger
- Stabilitet: Hovedversioner er sjældne, så clients behøver ikke opdateres ofte
- Fleksibilitet: Additive ændringer giver mulighed for udvikling uden at ødelægge noget
At foretage ændringer uden at ødelægge clients
Sikre ændringer (ingen versionsforøgelse)
// Tilføjelse af et nyt valgfrit felt
interface User {
id: string;
name: string;
email: string;
avatar?: string; // Nyt felt, valgfrit
}
// Tilføjelse af et nyt endpoint
GET /v1/users/123/preferences // Nyt endpoint
// Tilføjelse af en ny valgfri parameter
GET /v1/users?include_inactive=true // Ny parameter
Breaking changes (kræver versionsforøgelse)
// Fjernelse af et felt
// v1: { id, name, email }
// v2: { id, name } // email fjernet
// Ændring af felttype
// v1: { age: "25" } // streng
// v2: { age: 25 } // tal
// Ændring af endpoint-adfærd
// v1: GET /users returnerer alle brugere
// v2: GET /users returnerer paginerede brugere
Implementering af versionering
Versionering på router-niveau
// Express-eksempel
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get('/users/:id', v1UserController.get);
v2Router.get('/users/:id', v2UserController.get);
app.use('/v1', v1Router);
app.use('/v2', v2Router);
Versionering på controller-niveau
class UserController {
async getUser(req: Request, res: Response) {
const version = this.getVersion(req);
const user = await this.userService.findById(req.params.id);
if (version === 1) {
return res.json(this.serializeV1(user));
} else {
return res.json(this.serializeV2(user));
}
}
private serializeV1(user: User) {
return {
id: user.id,
name: user.name,
email: user.email
};
}
private serializeV2(user: User) {
return {
id: user.id,
fullName: user.name, // Omdøbt felt
emailAddress: user.email,
createdAt: user.createdAt // Nyt felt
};
}
}
Delt logik, forskellig serialisering
Nøglen er at dele forretningslogik og samtidig variere API-kontrakten:
// Delt servicelag
class UserService {
async findById(id: string): Promise<User> {
// Samme logik for alle versioner
}
}
// Versionsspecifikke serializere
const serializers = {
v1: new UserSerializerV1(),
v2: new UserSerializerV2()
};
// Controlleren bruger den passende serializer
const serializer = serializers[version];
return res.json(serializer.serialize(user));
Udfasningsstrategi
Kommunikér tidligt
HTTP/1.1 200 OK
Deprecation: Sun, 01 Jan 2025 00:00:00 GMT
Sunset: Sun, 01 Jul 2025 00:00:00 GMT
Link: </v2/users>; rel="successor-version"
Giv migreringsvejledninger
Dokumentér præcis, hvad der ændrede sig, og hvordan man migrerer:
## Migrering fra v1 til v2
### Ændringer i user-endpointet
| v1 | v2 | Bemærkninger |
|----|----|----|
| `name` | `fullName` | Omdøbt for klarhedens skyld |
| `email` | `emailAddress` | Omdøbt for klarhedens skyld |
| - | `createdAt` | Nyt felt |
### Nødvendige kodeændringer
```diff
- const name = user.name;
+ const name = user.fullName;
### Overvåg brug
Følg, hvilke versioner der bliver brugt:
```typescript
app.use((req, res, next) => {
const version = extractVersion(req);
metrics.increment('api.requests', { version });
next();
});
Almindelige fejl
For mange versioner
Hvis du har v1, v2, v3, v4, v5… versionerer du for aggressivt. Hver version har vedligeholdelsesomkostninger.
Løsning: Brug additive ændringer inden for versioner. Forøg kun versionen ved reelt brydende ændringer.
Inkonsistent versionering
Forskellige endpoints bruger forskellige versioneringsordninger.
Løsning: Vælg én tilgang, og hold fast i den på tværs af hele din API.
Ingen udfasningsperiode
At fjerne gamle versioner uden varsel.
Løsning: Annoncér udfasning mindst 6-12 måneder i forvejen. Overvåg brugen inden fjernelse.
Versionering af interne API’er
Tilføjelse af versioneringsoverhead til API’er, der kun bruges af dit eget team.
Løsning: Interne API’er kan ofte bare udvikle sig gennem koordinerede deployments.
Et eksempel fra den virkelige verden
Sådan strukturerede jeg versioneringen af en betalings-API:
/v1/payments # Original API (2020)
/v1/payments/intents # Tilføjet 2021, ingen versionsforøgelse
/v1/refunds # Tilføjet 2021, ingen versionsforøgelse
/v2/payments # Breaking changes (2023)
# - Ændrede beløb fra cent til decimaltal
# - Omstrukturerede fejlresponser
# - Fjernede forældede felter
Tidslinje:
- 2023-01: v2 annonceret, udfasning af v1
- 2023-06: v2 udgivet, v1 stadig understøttet
- 2024-01: sunset-advarselsmails for v1
- 2024-06: v1 fjernet
Konklusion
API-versionering behøver ikke at være kompliceret:
- Brug URL-sti-versionering for klarhed
- Udvikl inden for versioner ved hjælp af additive ændringer
- Opret kun nye versioner ved breaking changes
- Udfas på en ordentlig måde med lange varslingsperioder
Målet er at give dine API-forbrugere stabilitet, samtidig med at din API kan udvikle sig. En veludviklet API skaber tillid og gør integration lettere for alle.