Quelles sont les meilleures pratiques pour les ressources imbriquées REST?

301

Autant que je sache, chaque ressource individuelle ne devrait avoir qu'un seul chemin canonique . Dans l'exemple suivant, quels seraient les bons modèles d'URL?

Prenons par exemple une représentation représentative des entreprises. Dans cet exemple hypothétique, chaque entreprise possède 0 ou plusieurs départements et chaque département possède 0 ou plusieurs employés.

Un département ne peut exister sans une entreprise associée.

Un employé ne peut exister sans un service associé.

Maintenant, je trouverais la représentation naturelle des modèles de ressources.

  • /companies Une collection d'entreprises - Accepte une nouvelle entreprise. Obtenez pour toute la collection.
  • /companies/{companyId}Une entreprise individuelle. Accepte GET, PUT et DELETE
  • /companies/{companyId}/departmentsAccepte le POST pour un nouvel élément. (Crée un département au sein de l'entreprise.)
  • /companies/{companyId}/departments/{departmentId}/
  • /companies/{companyId}/departments/{departmentId}/employees
  • /companies/{companyId}/departments/{departmentId}/employees/{empId}

Compte tenu des contraintes, dans chacune des sections, je pense que cela a du sens s'il est un peu profondément imbriqué.

Cependant, ma difficulté vient si je veux lister ( GET) tous les employés dans toutes les entreprises.

Le modèle de ressources correspondant le plus étroitement à /employees(La collection de tous les employés)

Cela signifie-t-il que j'aurais dû /employees/{empId}aussi parce que si c'est le cas, il y a deux URI pour obtenir la même ressource?

Ou peut-être que le schéma entier devrait être aplati, mais cela signifierait que les employés sont un objet de niveau supérieur imbriqué.

Au niveau de base, /employees/?company={companyId}&department={deptId}la même vue des employés est exactement la même que le modèle le plus profondément imbriqué.

Quelle est la meilleure pratique pour les modèles d'URL dans lesquels les ressources appartiennent à d'autres ressources mais doivent être interrogeables séparément?

Nous s
la source
1
C'est presque exactement le problème opposé à celui décrit dans stackoverflow.com/questions/7104578/… bien que les réponses puissent être liées. Les deux questions concernent la propriété, mais cet exemple implique que l'objet de niveau supérieur n'est pas le propriétaire.
Wes
1
Exactement ce que je me demandais. Pour le cas d'utilisation donné, votre solution semble correcte, mais que se passe-t-il si la relation est une agrégation plutôt qu'une composition? Encore du mal à comprendre quelle est la meilleure pratique ici ... De plus, cette solution implique-t-elle uniquement la création de la relation, par exemple une personne existante est-elle employée ou crée-t-elle un objet personne?
Jakob O.
Cela crée une personne dans mon exemple fictif. La raison pour laquelle j'ai utilisé ces termes de domaine est un exemple raisonnablement compréhensible, bien que mimant mon problème réel. Avez-vous regardé la question liée qui pourrait vous empêcher davantage de vivre une relation d'agrégation.
Wes
J'ai divisé ma question en une réponse et une question.
Wes

Réponses:

152

Ce que vous avez fait est correct. En général, il peut y avoir plusieurs URI vers la même ressource - il n'y a pas de règles qui disent que vous ne devriez pas faire ça.

Et généralement, vous devrez peut-être accéder aux éléments directement ou en tant que sous-ensemble de quelque chose d'autre - donc votre structure est logique pour moi.

Tout simplement parce que les employés sont accessibles par département:

company/{companyid}/department/{departmentid}/employees

Cela ne veut pas dire qu'ils ne peuvent pas non plus être accessibles sous l'entreprise:

company/{companyid}/employees

Ce qui rendrait des employés pour cette entreprise. Cela dépend de ce dont votre client consommateur a besoin - c'est pour cela que vous devriez concevoir.

Mais j'espère que tous les gestionnaires d'URL utilisent le même code de support pour satisfaire les demandes afin que vous ne dupliquiez pas le code.

jeremyh
la source
11
Cela souligne l'esprit de RESTful, il n'y a pas de règles qui disent que vous devriez ou ne devriez pas faire si vous considérez d'abord une ressource significative . Mais en outre, je me demande quelle est la meilleure pratique pour ne pas dupliquer le code dans de tels scénarios.
abookyun
13
@abookyun si vous avez besoin des deux routes, alors le code de contrôleur répété entre elles peut être abstrait pour les objets de service.
bgcode
Cela n'a rien à voir avec REST. REST ne se soucie pas de la façon dont vous structurez la partie chemin de vos URL ... tout ce qui lui importe est des URI valides et, espérons-le, durables ...
redben
En conduisant à cette réponse, je pense que toute API où les segments dynamiques sont tous des identifiants uniques ne devrait pas avoir besoin de gérer plusieurs segments dynamiques ( /company/3/department/2/employees/1). Si l'API fournit des moyens d'obtenir chaque ressource, alors chacune de ces demandes peut être effectuée dans une bibliothèque côté client ou en tant que point de terminaison unique qui réutilise le code.
max
1
Bien qu'il n'y ait aucune interdiction, je considère qu'il est plus élégant de n'avoir qu'un seul chemin vers une ressource - simplifie tous les modèles mentaux. Je préfère également que les URI ne changent pas leur type de ressource en cas d'imbrication. par exemple, /company/*ne doit renvoyer que la ressource de l'entreprise et ne pas modifier du tout le type de ressource. Rien de tout cela n'est spécifié par REST - c'est généralement une préférence mal spécifiée - juste une préférence personnelle.
kashif
174

J'ai essayé les deux stratégies de conception - points de terminaison imbriqués et non imbriqués. J'ai trouvé que:

  1. si la ressource imbriquée a une clé primaire et que vous n'avez pas sa clé primaire parent, la structure imbriquée vous oblige à l'obtenir, même si le système n'en a pas réellement besoin.

  2. les points de terminaison imbriqués nécessitent généralement des points de terminaison redondants. En d'autres termes, vous aurez le plus souvent besoin du point de terminaison supplémentaire / employés pour pouvoir obtenir une liste des employés dans les différents services. Si vous avez / employés, qu'est-ce que / entreprises / services / employés vous achètent exactement?

  3. les points de terminaison d'imbrication n'évoluent pas aussi bien. Par exemple, vous n'aurez peut-être pas besoin de rechercher des employés maintenant, mais vous pourrez le faire plus tard et si vous avez une structure imbriquée, vous n'avez pas d'autre choix que d'ajouter un autre point de terminaison. Avec une conception non imbriquée, vous ajoutez simplement plus de paramètres, ce qui est plus simple.

  4. Parfois, une ressource peut avoir plusieurs types de parents. Il en résulte plusieurs points de terminaison renvoyant tous la même ressource.

  5. les points de terminaison redondants rendent les documents plus difficiles à écrire et rendent également l'API plus difficile à apprendre.

En bref, la conception non imbriquée semble permettre un schéma de point de terminaison plus flexible et plus simple.

Patc
la source
24
C'était très rafraîchissant de tomber sur cette réponse. J'utilise des points d'extrémité imbriqués depuis plusieurs mois maintenant après avoir appris que c'était la "bonne façon". Je suis arrivé à toutes les mêmes conclusions que vous avez énumérées ci-dessus. C'est beaucoup plus facile avec une conception non imbriquée.
user3344977
6
Vous semblez énumérer certains des inconvénients comme des avantages. «Il suffit de regrouper plus de paramètres en un seul point de terminaison» rend l'API plus difficile à documenter et à apprendre, et non l'inverse. ;-)
Drenmi
4
Pas un fan de cette réponse. Il n'est pas nécessaire d'introduire des points de terminaison redondants simplement parce que vous avez ajouté une ressource imbriquée. Ce n'est pas non plus un problème que la même ressource soit retournée par plusieurs parents, à condition que ces parents soient véritablement propriétaires de la ressource imbriquée. Ce n'est pas un problème pour obtenir une ressource parent d'apprendre à interagir avec les ressources imbriquées. Une bonne API REST découvrable devrait le faire.
Scottm
3
@Scottm - Un inconvénient des ressources imbriquées que j'ai rencontré est qu'il pourrait conduire à renvoyer des données incorrectes si les ID de ressource parent sont incorrects / incompatibles. En supposant qu'il n'y ait aucun problème d'autorisation, il est laissé à l'implémentation de l'API pour vérifier que la ressource imbriquée est bien un enfant de la ressource parent transmise. Si cette vérification n'est pas codée, la réponse de l'API peut être incorrecte, entraînant une corruption. Quelles sont vos pensées?
Andy Dufresne
1
Vous n'avez pas besoin des ID parent intermédiaires si les ressources finales ont toutes des ID uniques. Par exemple, pour obtenir l'employé par identifiant, vous avez GET / entreprises / départements / employés / {empId} ou pour obtenir tous les employés de l'entreprise 123, vous avez GET / entreprises / 123 / départements / employés / Garder le chemin hiérarchique rend plus évident comment vous pouvez accéder aux ressources intermédiaires pour filtrer / créer / modifier et aide à la découverte à mon avis.
PaulG
77

J'ai déplacé ce que j'ai fait de la question à une réponse où plus de gens sont susceptibles de le voir.

Ce que j'ai fait, c'est d'avoir les points de terminaison de création sur le point de terminaison imbriqué. Le point de terminaison canonique pour modifier ou interroger un élément n'est pas sur la ressource imbriquée .

Donc, dans cet exemple (énumérant simplement les points de terminaison qui modifient une ressource)

  • POST /companies/ crée une nouvelle société renvoie un lien vers la société créée.
  • POST /companies/{companyId}/departments lorsqu'un département est créé crée le nouveau département renvoie un lien vers /departments/{departmentId}
  • PUT /departments/{departmentId} modifie un département
  • POST /departments/{deparmentId}/employees crée un nouvel employé renvoie un lien vers /employees/{employeeId}

Il existe donc des ressources de niveau racine pour chacune des collections. Cependant, la création se trouve dans l' objet propriétaire .

Nous s
la source
4
J'ai également proposé le même type de conception. Je pense qu'il est intuitif de créer des choses comme celle-ci «où elles appartiennent», mais de pouvoir ensuite les répertorier globalement. Encore plus quand il y a une relation où une ressource DOIT avoir un parent. La création de cette ressource à l'échelle mondiale ne rend pas cela évident, mais le faire dans une sous-ressource comme celle-ci est parfaitement logique.
Joakim
Je suppose que vous avez utilisé un POSTsens PUT, et autrement.
Gerardo Lima
En fait non Notez que je n'utilise pas d'identifiants pré-attribués pour la création car le serveur dans ce cas est responsable du renvoi de l'identifiant (dans le lien). Par conséquent, écrire POST est correct (ne peut pas faire un get sur la même implémentation). Le put change cependant la ressource entière mais elle est toujours disponible au même endroit donc je la mets. PUT vs POST est une question différente et est également controversée. Par exemple stackoverflow.com/questions/630453/put-vs-post-in-rest
Wes
@Wes Même je préfère que les méthodes de modification des verbes soient sous le parent. Mais, voyez-vous que le passage du paramètre de requête pour la ressource globale est bien accepté? Ex: POST / départements avec le paramètre de requête company = company-id
Ayyappa
1
@Mohamad Si vous pensez que l'autre voie est plus facile à comprendre et à appliquer des contraintes, n'hésitez pas à donner une réponse. Il s'agit de rendre le mappage explicite dans ce cas. Cela pourrait fonctionner avec un paramètre mais c'est vraiment la question. Quel est le meilleur moyen.
Wes
35

J'ai lu toutes les réponses ci-dessus, mais il semble qu'ils n'aient pas de stratégie commune. J'ai trouvé un bon article sur les meilleures pratiques de Design API de Microsoft Documents . Je pense que vous devriez vous référer.

Dans les systèmes plus complexes, il peut être tentant de fournir des URI qui permettent à un client de naviguer à travers plusieurs niveaux de relations, tels que /customers/1/orders/99/products.Cependant, ce niveau de complexité peut être difficile à maintenir et est inflexible si les relations entre les ressources changent à l'avenir. Essayez plutôt de garder les URI relativement simples . Une fois qu'une application a une référence à une ressource, il devrait être possible d'utiliser cette référence pour rechercher des éléments liés à cette ressource. La requête précédente peut être remplacée par l'URI /customers/1/orderspour rechercher toutes les commandes du client 1, puis /orders/99/productsrechercher les produits dans cette commande.

.

Pointe

Évitez d'exiger des URI de ressources plus complexes que collection/item/collection.

Long Nguyen
la source
3
La référence que vous donnez est étonnante avec le point que vous vous démarquez de ne pas créer d'URI complexes.
vicco
Donc, quand je veux créer une équipe pour un utilisateur, que ce soit POST / teams (userId dans le corps) ou POST / users /: id / teams
coinhndp
@coinhndp Bonjour, Vous devez utiliser POST / équipes et vous pouvez obtenir userId après avoir autorisé le jeton d'accès. Je veux dire quand vous créez un truc, vous avez besoin d'un code d'autorisation, non? Je ne sais pas quel framework utilisez-vous, mais je suis sûr que vous pouvez obtenir userId dans le contrôleur API. Par exemple: dans l'API ASP.NET, appelez RequestContext.Principal à partir d'une méthode sur ApiController. Dans Spring Secirity, SecurityContextHolder.getContext (). GetAuthentication (). GetPrincipal () vous aidera. Dans AWS NodeJS Lambda, c'est cognito: nom d'utilisateur dans l'objet en-têtes.
Long Nguyen
Alors, quel est le problème avec le POST / utilisateurs /: id / équipes. Je pense qu'il est recommandé dans le document Microsoft que vous avez publié ci
coinhndp
@coinhndp Si vous créez une équipe en tant qu'administrateur, c'est bien. Mais, en tant qu'utilisateurs normaux, je ne sais pas pourquoi vous avez besoin de userId dans le chemin? Je suppose que nous avons user_A et user_B, que pensez-vous si user_A pourrait créer une nouvelle équipe pour user_B si user_A appelait POST / users / user_B / teams. Ainsi, pas besoin de passer userId dans ce cas, userId pourrait obtenir après autorisation. Mais, équipes /: id / projets est bon pour faire une relation entre équipe et projet par exemple.
Long Nguyen
10

L'aspect de vos URL n'a rien à voir avec REST. Tout va. Il s'agit en fait d'un "détail d'implémentation". Donc, tout comme la façon dont vous nommez vos variables. Tout ce qu'ils doivent être est unique et durable.

Ne perdez pas trop de temps à ce sujet, faites simplement un choix et respectez-le / soyez cohérent. Par exemple, si vous optez pour des hiérarchies, vous le faites pour toutes vos ressources. Si vous allez avec des paramètres de requête ... etc, tout comme les conventions de dénomination dans votre code.

Pourquoi Pour autant que je sache, une API "RESTful" doit être consultable (vous savez ... "Hypermedia en tant que moteur de l'état de l'application"), donc un client API ne se soucie pas de ce que sont vos URL tant qu'elles sont valide (il n'y a pas de référencement, pas d'humain qui a besoin de lire ces "URL amicales", sauf peut-être pour le débogage ...)

Le caractère agréable / compréhensible d'une URL dans une API REST ne vous intéresse qu'en tant que développeur d'API, pas en tant que client d'API, comme le serait le nom d'une variable dans votre code.

La chose la plus importante est que votre client API sache comment interpréter votre type de média. Par exemple, il sait que:

  • votre type de média a une propriété de liens qui répertorie les liens disponibles / liés.
  • Chaque lien est identifié par une relation (tout comme les navigateurs savent que le lien [rel = "stylesheet"] signifie que c'est une feuille de style ou rel = favico est un lien vers un favicon ...)
  • et il sait ce que ces relations signifient («sociétés» signifie une liste de sociétés, «recherche» signifie une URL basée sur un modèle pour effectuer une recherche sur une liste de ressources, «départements» signifie des départements de la ressource actuelle)

Voici un exemple d'échange HTTP (les corps sont en yaml car il est plus facile d'écrire):

Demande

GET / HTTP/1.1
Host: api.acme.io
Accept: text/yaml, text/acme-mediatype+yaml

Réponse: une liste de liens vers la ressource principale (entreprises, personnes, peu importe ...)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:04:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: this is your API's entrypoint (like a homepage)  
links:
  # could be some random path https://api.acme.local/modskmklmkdsml
  # the only thing the API client cares about is the key (or rel) "companies"
  companies: https://api.acme.local/companies
  people: https://api.acme.local/people

Demande: lien vers les entreprises (en utilisant la réponse précédente body.links.companies)

GET /companies HTTP/1.1
Host: api.acme.local
Accept: text/yaml, text/acme-mediatype+yaml

Réponse: une liste partielle des sociétés (sous les éléments), la ressource contient des liens connexes, comme un lien pour obtenir les deux sociétés suivantes (body.links.next), un autre lien (basé sur un modèle) pour rechercher (body.links.search)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:06:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: representation of a list of companies
links:
  # link to the next page
  next: https://api.acme.local/companies?page=2
  # templated link for search
  search: https://api.acme.local/companies?query={query} 
# you could provide available actions related to this resource
actions:
  add:
    href: https://api.acme.local/companies
    method: POST
items:
  - name: company1
    links:
      self: https://api.acme.local/companies/8er13eo
      # and here is the link to departments
      # again the client only cares about the key department
      department: https://api.acme.local/companies/8er13eo/departments
  - name: company2
    links:
      self: https://api.acme.local/companies/9r13d4l
      # or could be in some other location ! 
      department: https://api2.acme.local/departments?company=8er13eo

Donc, comme vous le voyez, si vous suivez les liens / relations, la façon dont vous structurez la partie chemin de vos URL n'a aucune valeur pour votre client API. Et si vous communiquez la structure de vos URL à votre client en tant que documentation, alors vous ne faites pas REST (ou du moins pas le niveau 3 selon le " modèle de maturité de Richardson ")

redben
la source
7
"La façon dont une URL est agréable / compréhensible dans une API REST ne vous intéresse qu'en tant que développeur d'API, pas en tant que client d'API, comme le serait le nom d'une variable dans votre code." Pourquoi cela ne serait-il PAS intéressant? C'est très important, si quelqu'un d'autre que vous-même utilise également l'API. Cela fait partie de l'expérience utilisateur, donc je dirais qu'il est très important que cela soit facile à comprendre pour les développeurs de clients API. Rendre les choses encore plus faciles à comprendre en reliant clairement les ressources est bien sûr un bonus (niveau 3 dans l'url que vous fournissez). Tout doit être intuitif et logique avec des relations claires.
Joakim
1
@Joakim Si vous créez une API de repos de niveau 3 (Hypertext As The Engine Of Application State), la structure du chemin de l'URL n'a absolument aucun intérêt pour le client (tant qu'elle est valide). Si vous ne visez pas le niveau 3, alors oui, c'est important et devrait être devinable. Mais le vrai REST est le niveau 3. Un bon article: martinfowler.com/articles/richardsonMaturityModel.html
redben
4
Je m'oppose à la création d'une API ou d'une interface utilisateur qui n'est pas conviviale pour les êtres humains. Niveau 3 ou non, je reconnais que lier les ressources est une excellente idée. Mais suggérer cela "permet de changer le schéma d'URL", c'est être déconnecté de la réalité et de la façon dont les gens utilisent les API. C'est donc une mauvaise recommandation. Mais dans le meilleur des mondes, tout le monde serait au niveau 3 REST. J'intègre des hyperliens ET utilise un schéma d'URL compréhensible par l'homme. Le niveau 3 n'exclut pas le premier, et on DEVRAIT s'en soucier à mon avis. Bon article cependant :)
Joakim
Il faut bien sûr se soucier de la maintenabilité et d'autres préoccupations, je pense que vous manquez le point de ma réponse: l'apparence de l'url ne mérite pas beaucoup de réflexion et vous devez "simplement faire un choix et s'en tenir à / être cohérente "comme je l'ai dit dans la réponse. Et dans le cas d'une API REST, du moins à mon avis, la convivialité n'est pas dans l'url, c'est surtout dans (le type de média) Quoi qu'il en soit, j'espère que vous comprenez mon point :)
redben
9

Je suis en désaccord avec ce genre de chemin

GET /companies/{companyId}/departments

Si vous voulez obtenir des départements, je pense qu'il vaut mieux utiliser une ressource / départements

GET /departments?companyId=123

Je suppose que vous avez une companiestable et une departmentstable puis des classes pour les mapper dans le langage de programmation que vous utilisez. Je suppose également que les départements peuvent être attachés à d'autres entités que les entreprises, donc une ressource / départements est simple, il est pratique d'avoir des ressources mappées sur des tables et vous n'avez pas besoin d'autant de points finaux car vous pouvez réutiliser

GET /departments?companyId=123

pour tout type de recherche, par exemple

GET /departments?name=xxx
GET /departments?companyId=123&name=xxx
etc.

Si vous souhaitez créer un service, le

POST /departments

la ressource doit être utilisée et le corps de la demande doit contenir l'ID de l'entreprise (si le service ne peut être lié qu'à une seule entreprise).

Maxime Laval
la source
1
Pour moi, c'est une approche acceptable uniquement si l'objet imbriqué a un sens en tant qu'objet atomique. S'ils ne le sont pas, il ne serait pas vraiment logique de les séparer.
Simme
C'est ce que j'ai dit, si vous voulez également pouvoir récupérer des départements, ce qui signifie que vous utiliserez un point de terminaison / départements.
Maxime Laval
2
Il peut également être judicieux d'autoriser l'inclusion de services via le chargement différé lors de la récupération d'une entreprise, par exemple GET /companies/{companyId}?include=departments, car cela permet à la fois à la société et à ses services d'être récupérés dans une seule requête HTTP. Fractal fait ça très bien.
Matthew Daly
1
Lorsque vous configurez des acls, vous souhaitez probablement limiter le /departmentspoint de terminaison pour qu'il ne soit accessible que par un administrateur, et que chaque entreprise n'accède à ses propres services que via `/ companies / {companyId} / départements`
Cuzox
@MatthewDaly OData le fait aussi bien avec $ expand
Rob Grant
1

Rails apporte une solution à ce problème: l' emboîtement peu profond .

Je pense que c'est une bonne chose car lorsque vous traitez directement avec une ressource connue, il n'est pas nécessaire d'utiliser des routes imbriquées, comme cela a été discuté dans d'autres réponses ici.

partydrone
la source