Essayer de concevoir une API pour des applications externes avec une vision du changement n'est pas facile, mais un peu de réflexion à l'avance peut vous faciliter la vie plus tard. J'essaie d'établir un schéma qui prendra en charge les modifications futures tout en restant rétrocompatible en laissant les gestionnaires de versions précédents en place.
La principale préoccupation de cet article est de savoir quel modèle doit être suivi pour tous les points finaux définis pour un produit/une entreprise donné.
Étant donné un modèle d'URL de base de https://rest.product.com/
J'ai conçu que tous les services relèvent de /api
de même que /auth
et d'autres points de terminaison non repos tels que /doc
. Par conséquent, je peux établir les points de terminaison de base comme suit:
https://rest.product.com/api/...
https://rest.product.com/auth/login
https://rest.product.com/auth/logout
https://rest.product.com/doc/...
Maintenant pour les points de terminaison eux-mêmes. La préoccupation concernant POST
, GET
, DELETE
n'est pas l'objectif principal de cet article et concerne les actions elles-mêmes.
Les points de terminaison peuvent être décomposés en espaces de noms et actions. Chaque action doit également se présenter de manière à prendre en charge les modifications fondamentales du type de retour ou des paramètres requis.
Prenant un service de chat hypothétique où les utilisateurs enregistrés peuvent envoyer des messages, nous pouvons avoir les points de terminaison suivants:
https://rest.product.com/api/messages/list/{user}
https://rest.product.com/api/messages/send
Maintenant, pour ajouter la prise en charge de la version pour les futurs changements d'API qui pourraient se casser. Nous pourrions soit ajouter la signature de la version après /api/
ou après /messages/
. Étant donné le point de terminaison send
, nous pourrions alors avoir les éléments suivants pour v1.
https://rest.product.com/api/v1/messages/send
https://rest.product.com/api/messages/v1/send
Donc ma première question est, quel est un endroit recommandé pour l'identifiant de version?
Donc, maintenant que nous avons établi que nous devons prendre en charge les versions antérieures, nous devons donc gérer le code pour chacune des nouvelles versions qui peuvent devenir obsolètes au fil du temps. En supposant que nous écrivons des points de terminaison dans Java nous pourrions gérer cela via des packages.
package com.product.messages.v1;
public interface MessageController {
void send();
Message[] list();
}
Cela a l'avantage que tout le code a été séparé par des espaces de noms où toute modification de rupture signifierait qu'une nouvelle copie des points de terminaison de service. Le désavantage de ceci est que tout le code doit être copié et que les corrections de bogues souhaitées doivent être appliquées aux versions nouvelles et antérieures doivent être appliquées/testées pour chaque copie.
Une autre approche consiste à créer des gestionnaires pour chaque point de terminaison.
package com.product.messages;
public class MessageServiceImpl {
public void send(String version) {
getMessageSender(version).send();
}
// Assume we have a List of senders in order of newest to oldest.
private MessageSender getMessageSender(String version) {
for (MessageSender s : senders) {
if (s.supportsVersion(version)) {
return s;
}
}
}
}
Cela isole maintenant la version de chaque point de terminaison lui-même et rend les correctifs de bogues compatibles avec le port en ne devant dans la plupart des cas être appliqués qu'une seule fois, mais cela signifie que nous devons faire un peu plus de travail sur chaque point de terminaison individuel pour le prendre en charge.
Il y a donc ma deuxième question "Quelle est la meilleure façon de concevoir REST code de service pour prendre en charge les versions précédentes."
Il y a donc ma deuxième question "Quelle est la meilleure façon de concevoir REST code de service pour prendre en charge les versions précédentes."
Une API très soigneusement conçue et orthogonale n'aura probablement jamais besoin d'être modifiée de manière incompatible en amont, donc le meilleur moyen est de ne pas avoir de versions futures.
Bien sûr, vous n'obtiendrez probablement pas vraiment cela du premier coup; Donc:
La première option de conception d'URI exprime mieux l'idée que vous versionnez l'intégralité de l'API. La seconde pourrait être interprétée comme une version des messages eux-mêmes. C'est donc mieux l'OMI:
rest.product.com/api/v1/messages/send
Pour la bibliothèque client, je pense que l'utilisation de deux implémentations complètes dans des packages séparés Java est plus propre, plus facile à utiliser et plus facile à entretenir.
Cela étant dit, il existe de meilleures méthodes pour faire évoluer les API que la gestion des versions. Je suis d'accord avec vous que vous devez vous y préparer, mais je pense au versioning comme méthode de dernier recours, à utiliser avec prudence et parcimonie. La mise à niveau représente un effort relativement important pour les clients. Il y a quelque temps, j'ai mis certaines de ces réflexions dans un article de blog:
http://theamiableapi.com/2011/10/18/api-design-best-practice-plan-for-evolution/
Pour REST versioning de l'API en particulier, vous trouverez cet article de Mark Nottingham utile:
http://www.mnot.net/blog/2011/10/25/web_api_versioning_smackdown
Une autre approche pour gérer le contrôle de version d'API consiste à utiliser Version dans les en-têtes HTTP. Comme
POST /messages/list/{user} HTTP/1.1
Host: http://rest.service.com
Content-Type: application/json
API-Version: 1.0 <----- like here
Cache-Control: no-cache
Vous pouvez analyser l'en-tête et le gérer de manière appropriée dans le backend.
Avec cette approche, les clients n'ont pas besoin de changer l'URL, juste l'en-tête. Et cela rend également les REST endpoints plus propres, toujours.
Si l'un des clients n'a pas envoyé l'en-tête de version, vous envoyez soit 400 - Mauvaise demande ou vous pouvez le gérer avec la version rétrocompatible de votre API.