Aller au contenu
Versionner une API sans la casser pendant cinq ansLecture : 0 %

Versionner une API sans la casser pendant cinq ans

Comment empiler des couches de compatibilité au-dessus d'un seul modèle courant, ce que chaque couche coûte pour toujours, et les changements qu'aucune couche ne sait absorber.

Par Elias Varen7 min de lectureMembres

/v2 est une promesse que vous ne tiendrez pas. Le jour où vous l'ouvrez, vous avez deux implémentations à maintenir. Les intégrateurs de /v1 ne migrent pas, parce que leur intégration fonctionne et que personne chez eux n'a de budget pour la refaire, et le premier correctif de sécurité doit être écrit deux fois. Deux ans plus tard, /v3 s'ajoute, et la moitié de votre code d'API sert des clients que vous n'osez pas couper.

Tenir cinq ans demande l'inverse : une seule implémentation, celle du modèle courant, et la compatibilité traitée comme une pile de petites traductions posées devant elle. Chaque changement cassant devient une couche. Le système fonctionne, et son prix tient à ce que les couches ne se retirent presque jamais.

Un modèle courant, des couches datées

Chaque intégrateur est épinglé à une version, identifiée par une date, celle du jour où il a créé sa clé, sauf s'il en demande explicitement une autre par un header. Votre code métier et vos contrôleurs ne connaissent que la version la plus récente, et une chaîne de transformations fait le lien entre les deux.

requête du client (version 2024-03-01)
        │  monte : chaque couche plus récente adapte la requête
        ▼
   modèle courant ── contrôleur ── réponse courante
        │  descend : chaque couche plus récente adapte la réponse
        ▼
réponse au client (forme 2024-03-01)

Un changement cassant s'écrit comme un objet qui sait faire les deux trajets :

PHP
interface VersionChange
{
    /** Date à partir de laquelle le nouveau comportement s'applique. */
    public function introducedOn(): string;

    /** Adapte une requête écrite pour la forme précédente. */
    public function upgradeRequest(string $route, array $payload): array;

    /** Ramène une réponse courante à la forme précédente. */
    public function downgradeResponse(string $route, array $payload): array;
}

final class SplitCustomerName implements VersionChange
{
    public function introducedOn(): string
    {
        return '2025-06-01';
    }

    public function upgradeRequest(string $route, array $payload): array
    {
        if ($route === 'customers.create' && isset($payload['name'])) {
            [$first, $last] = array_pad(explode(' ', $payload['name'], 2), 2, '');
            $payload['first_name'] = $first;
            $payload['last_name'] = $last;
            unset($payload['name']);
        }

        return $payload;
    }

    public function downgradeResponse(string $route, array $payload): array
    {
        if (isset($payload['first_name'], $payload['last_name'])) {
            $payload['name'] = trim($payload['first_name'].' '.$payload['last_name']);
            unset($payload['first_name'], $payload['last_name']);
        }

        return $payload;
    }
}

Pour un client épinglé au 1er mars 2024, on applique toutes les couches dont la date est postérieure, dans l'ordre chronologique à la montée et dans l'ordre inverse à la descente. Un client à jour n'en traverse aucune.

L'exemple mérite qu'on s'y arrête, car le découpage du nom sur la première espace est faux pour une partie des noms. Une couche de compatibilité fait au mieux avec l'information que l'ancienne forme contenait, et ce « au mieux » doit être écrit dans la documentation de la version.

Versionner une API sans la casser pendant cinq ans · Deepstack