Concepts de l'API REST

10

J'ai trois questions sur la conception de l'API REST sur lesquelles j'espère que quelqu'un pourra faire la lumière. J'ai cherché sans relâche pendant de nombreuses heures, mais je n'ai trouvé de réponses à mes questions nulle part (peut-être que je ne sais pas quoi chercher?).

question 1

Ma première question concerne les actions / RPC. Je développe une API REST depuis un certain temps et j'ai l'habitude de penser aux choses en termes de collections et de ressources. Cependant, je suis tombé sur quelques cas où le paradigme ne semble pas s'appliquer et je me demande s'il existe un moyen de concilier cela avec le paradigme REST.

Plus précisément, j'ai un cas où la modification d'une ressource entraîne la génération d'un e-mail. Cependant, à un stade ultérieur, l'utilisateur peut spécifiquement indiquer qu'il souhaite renvoyer l'e-mail envoyé précédemment. Lors du renvoi de l'e-mail, aucune ressource n'est modifiée. Aucun état n'est changé. C'est simplement une action qui doit se produire. L'action est liée au type de ressource spécifique.

Est-il approprié de mélanger une sorte d'appel à l'action avec un URI de ressource (par exemple /collection/123?action=resendEmail)? Serait-il préférable de spécifier l'action et de lui transmettre l'identifiant de la ressource (par exemple /collection/resendEmail?id=123)? Est-ce la mauvaise façon de procéder? Traditionnellement (au moins avec HTTP), l'action en cours est la méthode de requête (GET, POST, PUT, DELETE), mais celles-ci ne permettent pas vraiment des actions personnalisées avec une ressource.

question 2

J'utilise la partie chaîne de requête de l'URL pour filtrer l'ensemble des ressources retournées lors de l'interrogation d'une collection (par exemple /collection?someField=someval). Dans mon contrôleur API, je détermine ensuite quel type de comparaison il va faire avec ce champ et cette valeur. J'ai trouvé que ça ne marche vraiment pas. J'ai besoin d'un moyen pour permettre à l'utilisateur de l'API de spécifier le type de comparaison qu'il souhaite effectuer.

La meilleure idée que j'ai eue jusqu'à présent est de permettre à l'utilisateur de l'API de le spécifier comme un appendice au nom du champ (par exemple /collection?someField:gte=someval- pour indiquer qu'il doit renvoyer des ressources où someFieldest supérieur ou égal à (> =) ce qui somevalest . Est-ce une bonne idée? Une mauvaise idée? Si oui, pourquoi? Y a-t-il une meilleure façon de permettre à l'utilisateur de spécifier le type de comparaison à effectuer avec le champ et la valeur donnés?

question 3

Je vois souvent des URI qui ressemblent à quelque chose /person/123/dogspour obtenir le persons dogs. J'ai généralement évité quelque chose comme ça parce qu'en fin de compte, je pense qu'en créant un URI comme celui-ci, vous accédez simplement à une dogscollection filtrée par un personID spécifique . Ce serait équivalent à /dogs?person=123. Y a-t-il vraiment une bonne raison pour qu'un URI REST ait plus de deux niveaux de profondeur ( /collection/resource_id)?

Justin Warkentin
la source
10
Vous avez trois questions. Pourquoi ne pas les poster séparément?
anaximander
3
Il serait préférable de diviser cela en 3 questions distinctes. Un téléspectateur peut être en mesure de fournir une excellente réponse à une, mais pas à toutes les questions.
2
Je pense qu'ils sont tous liés. Le titre est un peu élevé mais cette question va aider beaucoup de gens et se retrouve facilement lors d'une recherche SE. Cette question devrait devenir un wiki communautaire une fois que suffisamment de votes et de substance auront été ajoutés. Il m'a fallu des semaines pour rechercher ce genre de choses.
Andrew T Finnell
1
Il aurait peut-être été préférable de les publier séparément, IDK. Cependant, comme @AndrewFinnell l'a mentionné, j'ai pensé que ce serait une bonne idée de garder les questions ensemble car ce sont les questions les plus difficiles que j'ai eues sur REST et ce serait bien pour les autres de pouvoir trouver les réponses ensemble.
Justin Warkentin

Réponses:

11

Est-il approprié de mélanger une sorte d'appel à l'action avec un URI de ressource (par exemple /collection/123?action=resendEmail)? Serait-il préférable de spécifier l'action et de lui transmettre l'identifiant de la ressource (par exemple /collection/resendEmail?id=123)? Est-ce la mauvaise façon de procéder? Traditionnellement (au moins avec HTTP), l'action en cours est la méthode de requête (GET, POST, PUT, DELETE), mais celles-ci ne permettent pas vraiment des actions personnalisées avec une ressource.

Je préfère modéliser cela d'une manière différente, avec une collection de ressources représentant les courriels qui doivent être envoyés; l'envoi sera traité par les internes du service en temps voulu, auquel cas la ressource correspondante sera supprimée. (Ou l'utilisateur pourrait SUPPRIMER la ressource plus tôt, provoquant l'annulation de la demande pour l'envoi.)

Quoi que vous fassiez, ne mettez pas de verbes dans le nom de la ressource! C'est le nom (et la partie requête est l'ensemble d'adjectifs). Verbes nominatifs bizarres REST!

J'utilise la partie chaîne de requête de l'URL pour filtrer l'ensemble des ressources retournées lors de l'interrogation d'une collection (par exemple /collection?someField=someval). Dans mon contrôleur API, je détermine ensuite quel type de comparaison il va faire avec ce champ et cette valeur. J'ai trouvé que ça ne marche vraiment pas. J'ai besoin d'un moyen pour permettre à l'utilisateur de l'API de spécifier le type de comparaison qu'il souhaite effectuer.

La meilleure idée que j'ai eue jusqu'à présent est de permettre à l'utilisateur de l'API de le spécifier en tant qu'appendice au nom du champ (par exemple /collection?someField:gte=someval- pour indiquer qu'il doit renvoyer des ressources où someField est supérieur ou égal à ( >=) quel qu'il somevalsoit). Est-ce une bonne idée? Une mauvaise idée? Si oui, pourquoi? Y a-t-il une meilleure façon de permettre à l'utilisateur de spécifier le type de comparaison à effectuer avec le champ et la valeur donnés?

Je préfère spécifier une clause de filtre générale et l'avoir comme paramètre de requête facultatif sur toute demande de récupération du contenu de la collection. Le client peut alors spécifier exactement comment restreindre l'ensemble retourné, de la manière que vous désirez. Je m'inquiéterais également un peu de la découvrabilité du langage de filtre / requête; plus vous le faites riche, plus il est difficile à découvrir pour des clients arbitraires. Une approche alternative qui, au moins théoriquement, traite de ce problème de découverte est de permettre de créer des sous-ressources de restriction de la collection, que les clients obtiennent en POSTANT un document décrivant la restriction à la ressource de collection. C'est toujours un léger abus, mais au moins c'est un que vous pouvez clairement rendre découvrable!

Ce type de découverte est l'une des choses que je trouve le moins forte avec REST.

Je vois souvent des URI qui ressemblent /person/123/dogsà des chiens. J'ai généralement évité quelque chose comme ça parce qu'en fin de compte, je pense qu'en créant un URI comme ça, vous accédez en fait à une collection de chiens filtrée par un ID de personne spécifique. Ce serait équivalent à /dogs?person=123. Y a-t-il vraiment une bonne raison pour qu'un URI REST ait plus de deux niveaux de profondeur ( /collection/resource_id)?

Lorsque la collection imbriquée est vraiment une sous-fonctionnalité des entités membres de la collection externe, il est raisonnable de les structurer en tant que sous-ressource. Par «sous-fonctionnalité», je veux dire quelque chose comme la relation de composition UML, où détruire la ressource externe signifie naturellement détruire la collection interne.

D'autres types de collection peuvent être modélisés comme une redirection HTTP; on /person/123/dogspeut donc en effet y répondre en faisant un 307 qui redirige vers /dogs?person=123. Dans ce cas, la collection n'est pas réellement une composition UML, mais plutôt une agrégation UML. La différence compte; c'est significatif!

Associés Donal
la source
2
Vous avez globalement des points solides. Cependant, bien que l' resendEmailaction puisse être gérée en créant une collection et en la publiant, cela semble moins naturel. En fait, je ne stocke rien dans la base de données lorsqu'un e-mail est renvoyé (pas besoin). Aucune ressource n'est modifiée, il s'agit donc simplement d'une action qui réussit ou échoue. Je n'ai pas pu retourner un ID de ressource qui existe au-delà de la durée de vie de l'appel, ce qui fait qu'une telle implémentation est un hack au lieu d'être RESTful. Ce n'est tout simplement pas une opération CRUD.
Justin Warkentin
3

Il est compréhensible d'être un peu confus sur la façon d'utiliser correctement REST en fonction de toutes les façons dont j'ai vu de grandes entreprises concevoir leurs API REST.

Vous avez raison en ce que REST est un système de collecte de ressources. Cela signifie Représentational State Transfer. Pas une bonne définition si vous me demandez. Mais les principaux concepts sont les 4 VERBES HTTP et être sans état.

La chose importante à noter est que vous n'avez que 4 VERBES avec REST. Ce sont GET, POST, PUT et DELETE. Votre resendexemple serait d'ajouter un nouveau verbe à REST. Cela devrait être un drapeau rouge.

question 1

Il est important de réaliser que l'appelant de votre API REST ne devrait pas avoir à savoir que l'exécution d'un PUTsur votre collection entraînerait la génération d'un e-mail. Ça sent une fuite pour moi. Ce qu'ils pouvaient savoir, c'est que l'exécution d'une PUTtâche pouvait entraîner des tâches supplémentaires qu'ils pourraient interroger plus tard. Ils le sauraient en effectuant une GETsur la ressource récemment créée. Cela GETretournerait la ressource et tous les Taskidentifiants de ressource qui lui sont associés. Vous pouvez ensuite interroger ces tâches plus tard pour déterminer leur statut et même en soumettre une nouvelle Task.

Vous avez quelques options.

REST - Approche basée sur les ressources de tâches

Créez une tasksressource dans laquelle vous pouvez soumettre des tâches spécifiques dans votre système pour effectuer des actions. Vous pouvez ensuite effectuer GETla tâche en fonction de la valeur IDrenvoyée pour déterminer son état.

Ou vous pouvez mélanger dans un SOAP over HTTPservice Web afin d'ajouter du RPC à votre architecture.

interroger toutes les tâches pour une ressource spécifique

GET http://server/api/myCollection/123/tasks

{ "tasks" :
    [ { "22333" : "http://server/api/tasks/223333" } ] 
}

exemple de ressource de tâche

PUT http://server/api/tasks

{ 
    "type" : "send-email" , 
    "parameters" : 
    { 
         "collection-type" : "foo" , 
         "collection-id" : "123" 
    } 
}

==> renvoie l'id de la tâche

223334

GET http://server/api/tasks/223334

{ 
    "status" : "complete" , 
    "date" : "whenever" 
}

REST - Utilisation de POST pour déclencher des actions

Vous pouvez toujours POSTajouter des données à une ressource. À mon avis, cela violerait l'esprit de REST, mais ce serait toujours conforme.

Vous pouvez faire un POST similaire à ceci:

POST http://server/api/collection/123

{ "action" : "send-email" }

Vous mettrez à jour la ressource 123 de la collection avec des données supplémentaires. Ces données supplémentaires sont essentiellement une action demandant au backend d'envoyer un e-mail pour cette ressource.

Le problème que j'ai avec ceci est qu'un GETsur la ressource retournera ces données mises à jour. Cependant, cela résoudrait vos besoins tout en étant RESTful.

SOAP - Service Web qui accepte les ressources obtenues à partir de REST

Créez un nouveau WebService dans lequel vous pouvez envoyer des e-mails en fonction de l'ID de ressource précédent à partir de l'API REST. Je n'entrerai pas dans les détails de SOAP ici car la question d'origine concerne REST et ces deux concepts / technologies ne doivent pas être comparés car ce sont des pommes et des oranges .

question 2

Vous avez également quelques options ici:

Il semble que de nombreuses grandes entreprises qui publient des API REST exposent une searchcollection qui n'est vraiment qu'un moyen de transmettre des paramètres de requête pour renvoyer des ressources.

GET http://server/api/search?q="type = myCollection & someField >= someval"

Qui retournerait une collection de ressources REST pleinement qualifiées telles que:

{
    "results" : 
       { [ 
             "location" : "http://server/api/myCollection/1",
             "location" : "http://server/api/myCollection/9",
             "location" : "http://server/api/myCollection/56"
         ]
       }
}

Ou vous pouvez autoriser quelque chose comme MVEL comme paramètre de requête.

question 3

Je préfère les sous-niveaux que d'avoir à remonter et interroger l'autre ressource avec un paramètre de requête. Je ne pense pas qu'il existe une règle d'une manière ou d'une autre. Vous pouvez implémenter les deux façons et permettre à l'appelant de décider lequel est le plus approprié en fonction de la façon dont il est entré pour la première fois dans le système.

Remarques

Je ne suis pas d'accord sur les commentaires de lisibilité des autres. Malgré ce que certains pourraient penser, REST n'est toujours pas destiné à la consommation humaine. C'est pour la consommation de la machine. Si je veux voir mes Tweets, j'utilise le site Web régulier de Twitters. Je n'effectue pas de REST GET avec leur API. Si je veux faire quelque chose par programme avec mes tweets, j'utilise leur API REST. Oui, les API doivent être compréhensibles, mais ce gten'est pas si mal, ce n'est tout simplement pas intuitif.

L'autre chose principale avec REST est que vous devriez pouvoir commencer à tout moment donné dans votre API et naviguer vers toutes les autres ressources associées SANS connaître à l'avance l'URL exacte des autres ressources. Les résultats du GETVERBE dans REST doivent renvoyer l'URL REST complète des ressources qu'il référence. Ainsi, au lieu d'une requête renvoyant l'ID d'un Personobjet, il retournerait l'URL entièrement qualifiée telle que http://server/api/people/13. Ensuite, vous pouvez toujours parcourir par programme les résultats même si l'URL a changé.

Réponse au commentaire

Dans le monde réel, il y a en fait des choses qui doivent se produire qui ne créent, lisent, mettent à jour ou suppriment (CRUD) une ressource.

Des actions supplémentaires peuvent être prises sur les ressources. Les bases de données relationnelles typiques prennent en charge le concept de procédures stockées. Ce sont des commandes supplémentaires qui peuvent être exécutées sur un ensemble de données. REST n'a pas intrinsèquement ce concept. Et il n'y a aucune raison que ce soit le cas. Ces types d'actions sont parfaits pour les services Web RPC ou SOAP.

C'est le problème général que je vois avec les API REST. Les développeurs n'aiment pas les limitations conceptuelles qui entourent REST, ils l'adaptent donc pour faire ce qu'ils veulent. Cela le rompt cependant d'être un service RESTful. Essentiellement, ces URL deviennent des GETappels sur des servlets de type pseudo-REST.

Vous avez quelques options:

  • Créer une ressource de tâche
  • Prise POSTen charge de données supplémentaires sur la ressource pour effectuer une action.
  • Ajoutez les commandes supplémentaires via un service Web SOAP.

Si vous avez utilisé un paramètre de requête quel VERBE HTTP utiliseriez-vous pour renvoyer l'e-mail?

  • GET- Est-ce que cela renvoie l'e-mail ET renvoie les données de la ressource? Et si un système mettait cette URL en cache et la traitait comme l'URL unique de cette ressource. Chaque fois qu'ils frappaient l'URL, il renvoyait un e-mail.
  • POST - Vous n'avez pas réellement envoyé de nouvelles données à la ressource, juste un paramètre de requête supplémentaire.

Sur la base de toutes les exigences données, faire un POSTsur la ressource avec des action fielddonnées POST résoudra le problème.

Andrew T Finnell
la source
3
Bien que REST implémenté via HTTP vous donne ces 4 verbes, je ne suis pas convaincu que ces verbes devraient être la fin. Dans le monde réel, il y a en fait des choses qui doivent se produire qui ne créent, lisent, mettent à jour ou suppriment (CRUD) une ressource. Renvoyer un e-mail est l'une de ces choses. Je n'ai pas besoin de stocker ou de modifier quoi que ce soit dans la base de données. C'est simplement une action qui réussit ou échoue.
Justin Warkentin
@JustinWarkentin Je comprends vos besoins. Mais cela ne fait pas de REST quelque chose qu'il n'est pas. L'ajout d'un nouveau verbe à l'URL est contraire à l'architecture REST. Je mettrai à jour ma réponse pour proposer une autre alternative qui serait RESTful.
Andrew T Finnell
@JustinWarkentin Découvrez 'REST - Utilisation de POST pour déclencher des actions' dans ma réponse.
Andrew T Finnell
0

Question 1: Est-il approprié de mélanger une sorte d'appel d'action avec un URI de ressource [ou] serait-il préférable de spécifier l'action et de lui transmettre l'ID de ressource?

Bonne question. Dans ce cas, je vous conseille d'utiliser cette dernière approche, à savoir spécifier l'action et lui transmettre un ID de ressource. De cette façon, lorsque votre ressource est modifiée pour la première fois, elle appelle à son tour l' /sendEmailaction (note latérale: pas besoin de l'appeler "renvoyer") en tant que demande RESTful distincte (que vous pourrez ensuite appeler encore et encore, indépendamment de la ressource en cours de modification ).

Question 2: concernant l'utilisation d'un opérateur de comparaison comme ceci:/collection?someField:gte=someval

Bien que cela soit techniquement correct, c'est probablement une mauvaise idée. L'un des principes clés de REST est la lisibilité. Je vous suggère de simplement passer l'opérateur de comparaison comme un autre paramètre, par exemple: /collection?someField=someval&operator=gteet bien sûr de concevoir votre API de sorte qu'il réponde à un cas par défaut (dans le cas où le operatorparamètre est laissé hors de l'URI).

Question 3: Existe - t-il vraiment une bonne raison pour qu'un URI REST ait plus de deux niveaux de profondeur?

Ouaip; pour l'abstraction. J'ai vu quelques API REST qui utilisent des couches d'abstraction à travers plusieurs niveaux d'URI, par exemple: /vehicles/cars/123ou /vehicles/bikes/123qui à leur tour vous permettent de travailler avec des informations utiles concernant les deux /vehicleset les /vehicles/bikescollections. Cela dit, je ne suis pas un grand fan de cette approche; vous aurez rarement besoin de le faire dans la pratique, et il est probable que vous puissiez repenser l'API pour utiliser seulement 2 niveaux.

Et oui, comme le suggèrent les commentaires ci-dessus, à l'avenir, il serait préférable de diviser vos questions en messages séparés;)

Kosta Kontos
la source
Je pense que mon exemple pour la question # 2 était trop simpliste. Je dois spécifier un opérateur de comparaison pour chaque champ utilisé pour filtrer la collection, pas seulement un, donc dans votre exemple, cela devrait être quelque chose comme /collection?field1=someval&field1Operator=gte&field2=someval&field2Operator=eq.
Justin Warkentin
0

Pour la question 2, une alternative différente peut être plus flexible: considérez chaque recherche comme une ressource que l'utilisateur crée avant d'utiliser.

disons que vous avez un conteneur "recherches", là vous faites un POST /api/searches/avec la spécification de requête sur le contenu. il peut s'agir d'un document JSON, XML ou même d'un document SQL, tout ce qui est plus facile pour vous. Si la requête est correctement analysée, une nouvelle recherche est créée en tant que nouvelle ressource avec son propre URI, disons/api/searches/q123/

Ensuite, le client peut simplement GET /api/searches/q123/récupérer les résultats de la requête.

Enfin, vous pouvez soit demander au client de supprimer la requête, soit la purger après la fermeture de la session.

Javier
la source
0

Est-il approprié de mélanger une sorte d'appel d'action avec un URI de ressource (par exemple / collection / 123? Action = resendEmail)? Serait-il préférable de spécifier l'action et de lui transmettre l'ID de ressource (par exemple / collection / resendEmail? Id = 123)? Est-ce la mauvaise façon de procéder? Traditionnellement (au moins avec HTTP), l'action en cours est la méthode de requête (GET, POST, PUT, DELETE), mais celles-ci ne permettent pas vraiment des actions personnalisées avec une ressource.

Non, ce n'est pas approprié, car les IRI servent à identifier les ressources et non les opérations (cependant ppl utilise cette méthode de substitution de méthode pendant un certain temps, dans les cas où l'utilisation de méthodes non POST et GET n'est pas prise en charge). Vous pouvez rechercher une méthode HTTP appropriée ou en créer une nouvelle. POST peut être votre ami dans ces cas (veuillez l'utiliser s'il ne trouve pas de méthode appropriée et que la demande n'est pas récupérée). Une autre approche pour faire des ressources à partir de l'envoi d'e-mails et POST /emailspeut donc envoyer des e-mails sans créer de véritable ressource. Btw. La structure d'URI ne porte pas de sémantique, donc d'un point de vue REST peu importe le type d'URI que vous utilisez. Ce qui compte, ce sont les métadonnées (par exemple , la relation de lien ) attribuées aux liens que vous avez envoyés aux clients.

La meilleure idée que j'ai trouvée jusqu'à présent est de permettre à l'utilisateur de l'API de le spécifier en tant qu'appendice au nom du champ (par exemple / collection? SomeField: gte = someval - pour indiquer qu'il doit retourner des ressources là où someField est supérieur à ou égal à (> =) quelque soit la valeur. Est-ce une bonne idée? Une mauvaise idée? Si oui, pourquoi? Y a-t-il une meilleure façon de permettre à l'utilisateur de spécifier le type de comparaison à effectuer avec le champ et la valeur donnés?

Vous n'avez pas à créer votre propre langage de requête. Je préfère utiliser une version déjà existante et ajouter une description de la requête aux métadonnées du lien. Vous devriez probablement utiliser un type de média RDF (par exemple JSON-LD) pour cela ou utiliser un type MIME personnalisé (afaik il n'y a pas de format non-RDF qui le supporte). Utiliser les normes existantes découplent votre client du serveur, c'est à cela que sert la contrainte d'interface uniforme.

Ce serait équivalent à / dogs? Person = 123. Y a-t-il vraiment une bonne raison pour qu'un URI REST ait plus de deux niveaux de profondeur (/ collection / resource_id)?

Comme je l'ai mentionné précédemment, la structure URI n'a pas d'importance du point de vue REST. Vous pouvez utiliser /x71fd823df2par exemple. Cela aurait toujours du sens pour les clients car ils vérifient les métadonnées affectées aux liens et non la structure URI. L'URI a pour principal objectif d'identifier les ressources. Dans la norme URI, ils indiquent que le chemin contient des données hiérarchiques et que la requête contient des données non hiérarchiques. Mais il peut être très subjectif ce qui est hiérarchique. C'est pourquoi vous rencontrez des URI profonds à plusieurs niveaux et des URI avec de longues requêtes.

J'ai cherché sans relâche pendant de nombreuses heures, mais je n'ai trouvé de réponses à mes questions nulle part (peut-être que je ne sais pas quoi chercher?).

Vous devriez lire au moins les contraintes REST de la dissertation Fielding , la norme HTTP et probablement les API Web de 3e génération de Markus.

inf3rno
la source