“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 :

  1. URL versioning (/api/v1, /api/v2)
  2. Backward compatible quand possible
  3. Deprecation graduelle (6 mois minimum)
  4. Communication proactive
  5. Monitoring usage

Éviter breaking changes :

  • Additive changes only
  • GraphQL si possible
  • BFF pattern

Et vous, comment gérez-vous le versioning ?