Je prévois de construire une API RESTfull mais il y a des questions architecturales qui créent des problèmes dans ma tête. Ajouter une logique métier backend aux clients est une option que j'aimerais éviter car la mise à jour de plusieurs plates-formes clientes est difficile à maintenir en temps réel lorsque la logique métier peut changer rapidement.
Disons que nous avons un article comme ressource (api/article), comment devrions-nous implémenter des actions comme publier, annuler la publication, activer ou désactiver et ainsi de suite, mais pour essayer de le garder aussi simple que possible?
1) Devrions-nous utiliser api/article/{id}/{action} car beaucoup de logique de backend peut y arriver comme pousser vers des emplacements distants ou changer plusieurs propriétés. La chose la plus difficile ici est probablement que nous devons renvoyer toutes les données d'article à l'API pour la mise à jour et le travail multi-utilisateur n'a pas pu être implémenté. Par exemple, l'éditeur pourrait envoyer des données plus anciennes de 5 secondes et remplacer le correctif qu'un autre journaliste vient de faire il y a 2 secondes et il n'y a aucun moyen que je puisse expliquer cela aux clients, car ceux qui publient un article ne sont en aucun cas liés à la mise à jour du contenu.
2) La création d'une nouvelle ressource peut également être une option, api/article- {action}/id, mais alors la ressource retournée ne serait pas article- {action} mais un article dont je ne suis pas sûr si cela est approprié. Aussi dans la classe d'article de code côté serveur gère le travail actuall sur les deux ressources et je ne suis pas sûr si cela va à l'encontre de la pensée REST
Toutes les suggestions sont les bienvenues ..
Je trouve les pratiques décrites ici utiles:
Qu'en est-il des actions qui ne rentrent pas dans le monde des opérations CRUD?
C'est là que les choses peuvent devenir floues. Il existe un certain nombre d'approches:
- Restructurez l'action pour qu'elle apparaisse comme un champ d'une ressource. Cela fonctionne si l'action ne prend pas de paramètres. Par exemple, une action activate pourrait être mappée sur un champ booléen
activated
et mise à jour via un PATCH vers la ressource.- Traitez-le comme une sous-ressource avec des principes RESTful. Par exemple, l'API de GitHub vous permet star a Gist with
PUT /gists/:id/star
et nstar avecDELETE /gists/:id/star
.- Parfois, vous n'avez vraiment aucun moyen de mapper l'action à une structure RESTful sensible. Par exemple, une recherche multi-ressources n'a pas vraiment de sens à appliquer au point de terminaison d'une ressource spécifique. Dans ce cas,
/search
aurait le plus de sens même si ce n'est pas une ressource. C'est OK - faites ce qui est juste du point de vue du consommateur d'API et assurez-vous qu'il est clairement documenté pour éviter toute confusion.
Les opérations entraînant des changements majeurs d'état et de comportement côté serveur, comme l'action "publier" que vous décrivez, sont difficiles à modéliser explicitement dans REST. Une solution que je vois souvent est de conduire implicitement un tel comportement complexe à travers les données.
Envisagez de commander des marchandises via une API REST exposée par un marchand en ligne. La commande est une opération complexe. Plusieurs produits seront emballés et expédiés, votre compte sera facturé et vous recevrez un reçu. Vous pouvez annuler votre commande pour une durée limitée et il y a bien sûr une garantie de remboursement intégral qui vous permet de renvoyer des produits pour un remboursement.
Au lieu d'une opération d'achat complexe, une telle API peut vous permettre de créer une nouvelle ressource, un bon de commande. Au début, vous pouvez y apporter les modifications que vous souhaitez: ajouter ou supprimer des produits, modifier l'adresse de livraison, choisir une autre option de paiement ou annuler votre commande. Vous pouvez faire tout cela parce que vous n'avez encore rien acheté, vous manipulez simplement des données sur le serveur.
Une fois votre commande d'achat terminée et votre délai de grâce écoulé, le serveur verrouille votre commande pour empêcher toute autre modification. Ce n'est qu'à ce moment que la séquence complexe des opérations commence, mais vous ne pouvez pas la contrôler directement, mais indirectement via les données que vous avez placées précédemment dans le bon de commande.
Selon votre description, "publier" pourrait être implémenté de cette façon. Au lieu d'exposer une opération, vous placez une copie du brouillon que vous avez examiné et souhaitez publier en tant que nouvelle ressource sous/publier. Cela garantit que les mises à jour ultérieures du brouillon ne seront pas publiées même si l'opération de publication elle-même se termine quelques heures plus tard.
nous devons renvoyer toutes les données d'article à l'API pour la mise à jour et le travail multi-utilisateur n'a pas pu être implémenté. Par exemple, l'éditeur pourrait envoyer des données plus anciennes de 5 secondes et remplacer le correctif qu'un autre journaliste vient de faire il y a 2 secondes et il n'y a aucun moyen que je puisse expliquer cela aux clients, car ceux qui publient un article ne sont en aucun cas liés à la mise à jour du contenu.
Ce genre de chose est un défi, peu importe ce que vous faites, c'est un problème très similaire au contrôle de source distribué (Mercurial, git, etc.), et la solution, orthographiée en HTTP/ReST, ressemble un peu.
Supposons que vous ayez deux utilisateurs, Alice et Bob, travaillant tous les deux sur /articles/lunch
. (pour plus de clarté, la réponse est en caractères gras)
Tout d'abord, Alice crée l'article.
PUT /articles/lunch HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0
Hey Bob, what do you want for lunch today?
301 Moved Permanently
Location: /articles/lunch/1
Le serveur n'a pas créé de ressource, car aucune "version" n'était associée à la demande (en supposant un identifiant /articles/{id}/{version}
. Pour effectuer la création, Alice a été redirigée vers l'url de l'article/version qu'elle va créer. L'agent utilisateur d'Alice réappliquera alors la demande à la nouvelle adresse.
PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0
Hey Bob, what do you want for lunch today?
201 Created
Et maintenant, l'article a été créé. ensuite, bob regarde l'article:
GET /articles/lunch HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk
301 Moved Permanently
Location: /articles/lunch/1
Bob y regarde:
GET /articles/lunch/1 HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk
200 Ok
Content-Type: text/plain
Hey Bob, what do you want for lunch today?
Il décide d'ajouter son propre changement.
PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk
Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?
301 Moved Permanently
Location: /articles/lunch/2
Comme avec Alice, Bob est redirigé vers l'endroit où il créera une nouvelle version.
PUT /articles/lunch/2 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk
Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?
201 Created
Enfin, Alice décide qu'elle aimerait ajouter à son propre article:
PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0
Hey Bob, what do you want for lunch today?
I was thinking about getting Sushi.
409 Conflict
Location: /articles/lunch/3
Content-Type: text/diff
---/articles/lunch/2
+++/articles/lunch/3
@@ 1,2 1,2 @@
Hey Bob, what do you want for lunch today?
-Does pizza sound good to you, Alice?
+I was thinking about getting Sushi.
Au lieu d'être redirigé normalement, un code d'état différent est renvoyé au client, 409
, qui indique à Alice que la version à partir de laquelle elle essayait de créer une branche a déjà été branchée. Les nouvelles ressources ont quand même été créées (comme le montre l'en-tête Location
) et les différences entre les deux ont été incluses dans le corps de la réponse. Alice sait maintenant que la demande qu'elle vient de faire doit être fusionnée.
Toute cette redirection est liée à la sémantique de PUT
, qui nécessite que de nouvelles ressources soient créées exactement là où la ligne de demande le demande. cela pourrait également enregistrer un cycle de demande en utilisant POST
à la place, mais alors le numéro de version devrait être codé dans la demande par une autre magie, ce qui me semblait moins évident à des fins d'illustration, mais serait probablement encore être préféré dans une véritable API pour minimiser les cycles de demande/réponse.
Voici un autre exemple qui ne traite pas du contenu des documents mais plutôt de l'état transitoire. (Je trouve le contrôle de version - étant donné qu'en général, chaque version peut être une nouvelle ressource - une sorte de problème facile.)
Supposons que je souhaite exposer un service exécuté sur une machine via un REST afin qu'il puisse être arrêté, démarré, redémarré, etc.).
Quelle est l'approche la plus reposante ici? POST/service? Command = restart, par exemple? Ou POST/service/state avec un corps de, disons, "en cours d'exécution"?
Ce serait bien de codifier les meilleures pratiques ici et de savoir si REST est la bonne approche pour ce type de situation.
Deuxièmement, supposons que je souhaite conduire une action à partir d'un service qui n'affecte pas son propre état, mais déclenche plutôt un effet secondaire. Par exemple, un service de messagerie qui envoie un rapport, construit au moment de l'appel, à un tas d'adresses e-mail.
GET/report pourrait être un moyen d'obtenir moi-même une copie du rapport; mais que se passe-t-il si nous voulons pousser vers le côté serveur d'autres actions telles que l'envoi d'e-mails comme je l'ai dit ci-dessus. Ou en écrivant dans une base de données.
Ces cas dansent autour d'un fossé ressources-action, et je vois des moyens de les gérer de manière orientée REST, mais franchement, cela ressemble un peu à un hack pour le faire. La question clé est peut-être de savoir si une API REST doit prendre en charge les effets secondaires en général.
REST est orienté données et en tant que telles, les ressources fonctionnent mieux comme des "choses" que comme des actions. La sémantique implicite des méthodes http; GET, PUT, DELETE, etc. servent à renforcer l'orientation. POST bien sûr, est l'exception.
Une ressource peut être un mélange de données, par exemple. contenu de l'article; et les métadonnées ie. publié, verrouillé, révision. Il existe de nombreuses autres façons de découper les données, mais vous devez d'abord passer par ce à quoi ressemblera le flux de données afin de déterminer la plus optimale (le cas échéant). Par exemple, il se peut que les révisions soient leur propre ressource sous l'article comme le suggère TokenMacGuy.
En ce qui concerne la mise en œuvre, je ferais probablement quelque chose comme ce que suggère TockenMacGuy. J'ajouterais également des champs de métadonnées sur l'article, pas de révision, comme "verrouillé" et "publié".
Ne pensez pas que cela manipule directement l'état de l'article. Au lieu de cela, vous mettez un ordre de changement demandant que l'article soit créé.
Vous pouvez modéliser le placement d'un ordre de modification comme créant une nouvelle ressource d'ordre de changement (POST). Il y a beaucoup d'avantages. Par exemple, vous pouvez spécifier une date et une heure futures de publication de l'article dans le cadre de l'ordre de modification et laisser le serveur s'inquiéter de la façon dont il est implémenté.
Si la publication n'est pas un processus instantané, il n'est pas nécessaire d'attendre sa fin pour revenir au client. Vous reconnaissez simplement que l'ordre de modification a été créé et renvoyez l'ID de l'ordre de modification. Vous pouvez ensuite utiliser l'URL correspondant à cet ordre de modification pour partager le statut de l'ordre de modification.
Un aperçu clé pour moi était de reconnaître que cette métaphore d'ordre de changement n'est qu'une autre façon de décrire la programmation orientée objet. Au lieu de ressources, nous appelons alors des objets. Au lieu de changer les ordres, nous les appelons des messages. Une façon d'envoyer un message de A à B dans OO est d'avoir A appeler une méthode sur B. Une autre façon de le faire, en particulier lorsque A et B sont dans des ordinateurs différents, est d'avoir A créez un nouvel objet, M, et envoyez-le à B. REST formalise simplement ce processus.
Si je vous comprends bien, je pense que ce que vous avez est plus un problème de détermination des "règles commerciales" qu'un problème technique.
Le fait qu'un article puisse être écrasé pourrait être résolu en introduisant des niveaux d'autorisation où les utilisateurs seniors peuvent remplacer les versions des utilisateurs juniors. , etc.), vous pouvez surmonter ce problème. Vous pouvez également donner à l'utilisateur la possibilité de sélectionner une version donnée soit en combinant l'heure de soumission et le numéro de version.
Dans tous les cas ci-dessus, votre service doit implémenter les règles métier que vous définissez. Vous pouvez donc appeler le service avec les paramètres: ID utilisateur, article, version, action (lorsque la version est facultative, cela dépend encore une fois de vos règles métier).