Une extension de gestion de stocks pour magasins de bricolage expose depuis 2016 une API REST sous le namespace stocks/v1, consommée par une douzaine d’intégrations tierces développées par différents clients au fil des années : applications mobiles internes, tableaux de bord Excel connectés via macro, scripts de synchronisation vers des ERP variés. L’équipe qui maintient l’extension souhaite désormais changer la structure de la réponse pour un point de terminaison central, en renommant un champ ambigu et en modifiant le format d’une date, un changement jugé nécessaire mais incompatible avec les intégrations existantes qui dépendent de l’ancienne structure.
Casser purement et simplement ce contrat aurait cassé toutes les intégrations tierces du jour au lendemain, sans préavis, pour des clients qui n’ont pas forcément les ressources techniques pour réagir dans l’urgence. La solution retenue s’appuie sur une propriété que l’API REST de WordPress porte nativement depuis sa conception : le namespace inclut par convention un numéro de version.
Le namespace comme unité de versionnement
Chaque appel à register_rest_route() prend en premier argument un namespace, généralement de la forme mon-domaine/v1. Rien n’empêche de déclarer un second namespace, mon-domaine/v2, pour un ensemble de routes au contrat différent, tout en conservant le premier namespace parfaitement fonctionnel pour les intégrations qui n’ont pas encore migré :

function stocks_lire_article( int $id ) : ?array {
$article = get_post( $id );
if ( ! $article || 'article_stock' !== $article->post_type ) {
return null;
}
return array(
'id' => $article->ID,
'reference' => get_post_meta( $id, '_reference', true ),
'quantite' => (int) get_post_meta( $id, '_quantite', true ),
'maj' => $article->post_modified_gmt,
);
}
add_action( 'rest_api_init', function () {
$args_id = array(
'id' => array(
'validate_callback' => function ( $valeur ) {
return is_numeric( $valeur );
},
),
);
register_rest_route( 'stocks/v1', '/articles/(?P<id>\d+)', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'stocks_v1_article',
'permission_callback' => 'stocks_peut_lire',
'args' => $args_id,
) );
register_rest_route( 'stocks/v2', '/articles/(?P<id>\d+)', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'stocks_v2_article',
'permission_callback' => 'stocks_peut_lire',
'args' => $args_id,
) );
} );
function stocks_peut_lire() {
return current_user_can( 'read_private_posts' );
}
function stocks_v1_article( WP_REST_Request $request ) {
$donnees = stocks_lire_article( (int) $request['id'] );
if ( null === $donnees ) {
return new WP_Error( 'stocks_introuvable', 'Article inconnu.', array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'id' => $donnees['id'],
'ref' => $donnees['reference'],
'qte' => $donnees['quantite'],
'maj' => mysql2date( 'd/m/Y', $donnees['maj'] ),
) );
}
function stocks_v2_article( WP_REST_Request $request ) {
$donnees = stocks_lire_article( (int) $request['id'] );
if ( null === $donnees ) {
return new WP_Error( 'stocks_introuvable', 'Article inconnu.', array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'id' => $donnees['id'],
'reference' => $donnees['reference'],
'quantite_disponible' => $donnees['quantite'],
'modifie_le' => mysql_to_rfc3339( $donnees['maj'] ),
) );
}
Le point essentiel tient dans la fonction stocks_lire_article() : elle porte la logique métier, une seule fois. Les deux versions ne sont plus que deux présentations d’une même donnée. Quand une règle change, par exemple la façon de calculer la quantité disponible, la correction profite immédiatement aux deux contrats. C’est ce qui rend la cohabitation supportable dans la durée : on maintient deux adaptateurs minces, pas deux copies du code.
Ce qui change vraiment entre deux versions
Un nouveau numéro de version se justifie quand le contrat casse : un champ renommé, un type modifié, un format de date différent, une route supprimée. Il ne se justifie pas pour un ajout. Ajouter un champ à la réponse de la version 1 ne gêne aucun client raisonnable, qui ignore simplement ce qu’il ne connaît pas. Réserver la version 2 aux changements incompatibles évite de multiplier les espaces de noms, et donc les tests et la documentation à tenir à jour.
Dans l’exemple, trois différences sont incompatibles : le nom du champ de quantité, le nom du champ de date, et surtout le format de cette date. Une intégration qui analyse d/m/Y ne saurait pas lire une date au format ISO 8601. C’est exactement le genre de changement qu’un client découvre en production, le lundi matin, si on l’a modifié sur place.
Annoncer la fin de la version 1
Une dépréciation n’a d’effet que si les clients peuvent la lire. La norme décrite dans la RFC 8594 définit un en-tête HTTP Sunset, qui donne la date de retrait d’une ressource. On peut l’ajouter à toutes les réponses de l’ancien espace de noms avec le filtre rest_post_dispatch :
add_filter( 'rest_post_dispatch', function ( $reponse, $serveur, $requete ) {
if ( 0 === strpos( $requete->get_route(), '/stocks/v1/' ) ) {
$reponse->header( 'Sunset', 'Wed, 31 Jan 2024 23:59:59 GMT' );
$reponse->header(
'Link',
'<' . rest_url( 'stocks/v2/' ) . '>; rel="successor-version"',
false
);
}
return $reponse;
}, 10, 3 );
Un en-tête ne suffit pas, car personne ne lit les en-têtes de ses propres appels. Complétez-le par un courriel aux responsables d’intégration connus, une page de documentation qui liste les différences champ par champ, et une date ferme, annoncée plusieurs mois à l’avance. Six mois est un délai courant pour des intégrations maintenues par des tiers ; ajustez selon le nombre de clients concernés.
Mesurer avant de couper
La question la plus utile avant de supprimer la version 1 est : qui l’appelle encore ? Une façon simple de répondre consiste à compter les appels par route et par jour, dans une option ou une table légère, à partir du même filtre rest_post_dispatch. Si, trois mois après l’annonce, deux intégrations appellent encore l’ancienne version, vous savez exactement qui prévenir, au lieu de couper en espérant que personne n’ait besoin du service. Gardez ce compteur sobre : identifiez les clients par leur jeton ou leur utilisateur, pas par leur adresse IP, afin de ne pas accumuler de données personnelles inutiles.
Un contrat d’API se respecte comme une clause de contrat : on le modifie par avenant, avec un préavis, et jamais en silence.
Les pièges de la cohabitation
- Dupliquer la logique métier dans chaque version : au bout d’un an, les deux branches ne se comportent plus pareil et personne ne sait laquelle est juste.
- Modifier discrètement la version 1 « pour corriger un détail » : pour un client, un détail corrigé est un contrat changé.
- Oublier les tests automatisés de l’ancienne version : ils sont la seule garantie que la version 1 continue de répondre comme avant.
- Laisser les deux versions divergir sur les permissions : une route plus permissive dans l’ancienne version expose des données que la nouvelle protège.
- Annoncer une date de retrait sans jamais la tenir : la prochaine annonce ne sera plus prise au sérieux.
Quand ne pas créer de nouvel espace de noms
Si l’API n’a que des consommateurs que vous maîtrisez, par exemple le script JavaScript de votre propre extension, un versionnement formel est superflu : modifiez les deux côtés dans la même livraison. Le versionnement protège des clients que vous ne contrôlez pas. De même, un changement de comportement interne qui ne modifie ni la forme ni le sens de la réponse, comme une optimisation de requête, n’a rien à faire dans une nouvelle version.
Conclusion
Le numéro de version dans l’espace de noms est une convention vieille comme l’API REST de WordPress, et elle suffit à faire cohabiter deux contrats. La discipline tient en quatre gestes : une logique partagée, deux adaptateurs minces, une dépréciation annoncée par écrit et par en-tête, et un retrait décidé sur des mesures plutôt que sur des impressions.