“On doit changer ce endpoint mais 500 clients l’utilisent.” Le versioning d’API résout ce problème. Voici les stratégies qui fonctionnent vraiment en production.
Le problème : Breaking changes
Scénario classique
// V1 API (1000 clients utilisent ça)
GET /api/users/123
{
"id": 123,
"name": "Alice",
"email": "alice@example.com"
}
// V2 : On veut séparer prénom/nom
GET /api/users/123
{
"id": 123,
"firstName": "Alice", // Breaking change!
"lastName": "Smith",
"email": "alice@example.com"
}
// Résultat : 1000 clients cassés
Besoin : Faire évoluer l’API sans casser l’existant.
Stratégies de versioning
Strategy 1 : URL versioning
GET /api/v1/users/123 → Ancien format
GET /api/v2/users/123 → Nouveau format
Avantages :
- Simple et explicite
- Cache HTTP friendly
- Routage facile
Inconvénients :
- Duplication code potentielle
- URLs multiples pour même resource
Quand utiliser : APIs publiques, breaking changes majeurs
Strategy 2 : Header versioning
GET /api/users/123
Accept: application/vnd.myapi.v2+json
→ Returns V2 format
GET /api/users/123
Accept: application/vnd.myapi.v1+json
→ Returns V1 format
Avantages :
- URL unique
- RESTful pur
- Flexible
Inconvénients :
- Moins évident (hidden in headers)
- Cache complexe
- Debugging plus dur
Quand utiliser : APIs internes, contrôle client fort
Strategy 3 : Query parameter
GET /api/users/123?version=2
GET /api/users/123?version=1
Avantages :
- Simple
- Visible dans URL
Inconvénients :
- Pas RESTful
- Cache issues
Quand utiliser : Quick fix, legacy support temporaire
Strategy 4 : Content negotiation
GET /api/users/123
Accept: application/json; version=2
→ Returns V2
Avantages :
- Standard HTTP
- Flexible
Inconvénients :
- Complexe à implémenter
- Peu utilisé en pratique
Recommendation : URL versioning
Pour 80% des cas :
/api/v1/... → Version actuelle stable
/api/v2/... → Nouvelle version en beta
Simple, explicite, fonctionne.
Backward compatibility patterns
Pattern 1 : Additive changes only
// V1
{
"id": 123,
"name": "Alice"
}
// V2 : Ajouter field (backward compatible)
{
"id": 123,
"name": "Alice",
"avatar": "https://..." // Nouveau field, V1 l'ignore
}
// ✅ V1 clients still work
Règle : Toujours ajouter, jamais retirer ou renommer.
Pattern 2 : Default values
// V1 : Pas de pagination
GET /api/users
// V2 : Pagination avec defaults
GET /api/users
// → Équivalent à GET /api/users?page=1&limit=20
// ✅ V1 clients get default behavior
Pattern 3 : Deprecation graduelle
// V2 : Nouveau field + ancien deprecated
{
"id": 123,
"name": "Alice", // @deprecated Use firstName/lastName
"firstName": "Alice",
"lastName": "Smith"
}
// Header warning
Warning: 299 - "Field 'name' is deprecated, use 'firstName'/'lastName'"
Timeline :
Month 1-3: V2 lancée, V1 deprecated warning
Month 4-6: V1 encore supportée, warnings plus visibles
Month 7+: V1 retirée (après 6 mois de transition)
Pattern 4 : Adapter pattern
// V1 interface (legacy)
interface UserV1 {
id: number;
name: string;
}
// V2 interface (nouveau)
interface UserV2 {
id: number;
firstName: string;
lastName: string;
}
// Adapter V2 → V1
function toV1(userV2: UserV2): UserV1 {
return {
id: userV2.id,
name: `${userV2.firstName} ${userV2.lastName}`
};
}
// V1 endpoint utilise V2 data + adapter
app.get('/api/v1/users/:id', async (req, res) => {
const userV2 = await getUserV2(req.params.id);
const userV1 = toV1(userV2);
res.json(userV1);
});
// → V1 maintenu sans code dupliqué
Pattern 5 : Feature flags per version
const features = {
v1: {
newPagination: false,
expandedFields: false
},
v2: {
newPagination: true,
expandedFields: true
}
};
app.get('/api/:version/users', (req, res) => {
const version = req.params.version; // 'v1' or 'v2'
const config = features[version];
if (config.newPagination) {
// Nouvelle pagination
} else {
// Ancienne pagination
}
});
Implementation : Express.js exemple
// Structure projet
/src
/v1
/routes
users.ts
/controllers
users.controller.ts
/v2
/routes
users.ts
/controllers
users.controller.ts
/shared
/models
user.model.ts
/services
user.service.ts
// app.ts
import express from 'express';
import v1Routes from './v1/routes';
import v2Routes from './v2/routes';
const app = express();
// Mount versions
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
// Default to latest version
app.use('/api', v2Routes);
V1 controller :
// v1/controllers/users.controller.ts
import { UserService } from '../../shared/services/user.service';
import { toV1Format } from '../adapters/user.adapter';
export class UsersControllerV1 {
async getUser(req, res) {
const user = await UserService.findById(req.params.id);
// Convertir au format V1
const userV1 = toV1Format(user);
res.json(userV1);
}
}
V2 controller :
// v2/controllers/users.controller.ts
import { UserService } from '../../shared/services/user.service';
export class UsersControllerV2 {
async getUser(req, res) {
const user = await UserService.findById(req.params.id);
// Format V2 natif
res.json(user);
}
}
Migration clients : Le plan
Phase 1 : Annonce (Mois 0)
## Breaking Change Announcement
**What:** User API v2 released
**Why:** Better naming, more fields
**When:** V1 deprecated in 6 months
### Migration guide
[Detailed guide with examples]
### Support
- Slack: #api-migration
- Email: api-support@company.com
Phase 2 : Deprecation headers (Mois 1-3)
HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 1 Jun 2025 12:00:00 GMT
Link: </api/v2/users>; rel="successor-version"
{
"id": 123,
"name": "Alice"
}
Phase 3 : Monitoring (Mois 1-6)
// Track V1 usage
app.use('/api/v1', (req, res, next) => {
metrics.increment('api.v1.usage', {
endpoint: req.path,
client: req.headers['user-agent']
});
next();
});
// Dashboard
┌────────────────────────────────┐
│ V1 API Usage │
├────────────────────────────────┤
│ Total calls: 12,500/day │
│ Unique clients: 45 │
│ │
│ Top clients still on V1: │
│ 1. mobile-app (8k calls/day) │
│ 2. partner-acme (3k) │
│ 3. legacy-cron (1k) │
└────────────────────────────────┘
Phase 4 : Outreach (Mois 4-5)
Email to top V1 users:
Subject: Action Required: Migrate to V2 by June 1st
Hi {client_name},
Your app still uses our V1 API (12k calls/day).
V1 will be retired on June 1st, 2025.
Migration guide: [link]
Need help? Schedule call: [calendly]
We're here to help!
Phase 5 : Cutover (Mois 6)
// Mois 6 : Retirer V1
app.use('/api/v1', (req, res) => {
res.status(410).json({
error: 'V1 API retired',
message: 'Please migrate to V2',
guide: 'https://docs.api.com/migration-v1-to-v2'
});
});
OpenAPI / Swagger
openapi: 3.0.0
info:
title: My API
version: 2.0.0
servers:
- url: https://api.example.com/v2
description: Current version
- url: https://api.example.com/v1
description: Deprecated (sunset 2025-06-01)
paths:
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/UserV2'
components:
schemas:
UserV2:
type: object
properties:
id:
type: integer
firstName:
type: string
lastName:
type: string
Testing multi-version
// Test V1 et V2 en parallel
describe('Users API', () => {
describe('V1', () => {
it('should return user with name field', async () => {
const res = await request(app)
.get('/api/v1/users/123')
.expect(200);
expect(res.body).toMatchObject({
id: 123,
name: expect.any(String)
});
});
});
describe('V2', () => {
it('should return user with firstName/lastName', async () => {
const res = await request(app)
.get('/api/v2/users/123')
.expect(200);
expect(res.body).toMatchObject({
id: 123,
firstName: expect.any(String),
lastName: expect.any(String)
});
});
});
describe('V1 → V2 consistency', () => {
it('should return same data (formatted differently)', async () => {
const v1 = await request(app).get('/api/v1/users/123');
const v2 = await request(app).get('/api/v2/users/123');
expect(v1.body.id).toBe(v2.body.id);
expect(v1.body.name).toBe(`${v2.body.firstName} ${v2.body.lastName}`);
});
});
});
Checklist : Breaking change ?
Est-ce un breaking change ?
✅ Renommer field → OUI
✅ Supprimer field → OUI
✅ Changer type (string → number) → OUI
✅ Changer format (date string format) → OUI
✅ Changer enum values → OUI
✅ Changer error codes → OUI
❌ Ajouter nouveau field → NON
❌ Ajouter nouvel endpoint → NON
❌ Ajouter enum value → NON (usually)
❌ Deprecate (avec support) → NON
Alternatives : Éviter breaking changes
GraphQL : Natural versioning
# V1
type User {
id: ID!
name: String!
}
# V2 : Ajouter fields sans casser V1
type User {
id: ID!
name: String! @deprecated(reason: "Use firstName/lastName")
firstName: String!
lastName: String!
}
# V1 clients query
{
user(id: 123) {
id
name # Encore supporté
}
}
# V2 clients query
{
user(id: 123) {
id
firstName
lastName
}
}
Avantage : Clients demandent seulement ce qu’ils veulent.
BFF (Backend for Frontend)
Mobile app → BFF Mobile → Core API V2
Web app → BFF Web → Core API V2
Partner → BFF Partner → Core API V2
# Chaque BFF adapte format selon besoin client
# Core API peut évoluer sans casser clients
Conclusion
Versioning d’API n’est pas optionnel.
C’est une promesse à vos clients :
- Stabilité
- Prévisibilité
- Temps de migration
Best practices :
- URL versioning (/api/v1, /api/v2)
- Backward compatible quand possible
- Deprecation graduelle (6 mois minimum)
- Communication proactive
- Monitoring usage
Éviter breaking changes :
- Additive changes only
- GraphQL si possible
- BFF pattern
Et vous, comment gérez-vous le versioning ?