¿Cuáles son las mejores prácticas para los recursos anidados REST?


301

Por lo que puedo decir, cada recurso individual debe tener solo una ruta canónica . Entonces, en el siguiente ejemplo, ¿cuáles serían los buenos patrones de URL?

Tomemos como ejemplo un resto de representación de empresas. En este ejemplo hipotético, cada compañía posee 0 o más departamentos y cada departamento posee 0 o más empleados.

Un departamento no puede existir sin una empresa asociada.

Un empleado no puede existir sin un departamento asociado.

Ahora me parece que la representación natural de los patrones de recursos es.

  • /companies Una colección de compañías - Acepta poner para una nueva compañía. Obtenga para toda la colección.
  • /companies/{companyId}Una empresa individual. Acepta GET, PUT y DELETE
  • /companies/{companyId}/departmentsAcepta POST para un nuevo artículo. (Crea un departamento dentro de la empresa).
  • /companies/{companyId}/departments/{departmentId}/
  • /companies/{companyId}/departments/{departmentId}/employees
  • /companies/{companyId}/departments/{departmentId}/employees/{empId}

Dadas las restricciones, en cada una de las secciones, siento que esto tiene sentido si está un poco anidado.

Sin embargo, mi dificultad viene si quiero enumerar ( GET) a todos los empleados de todas las empresas.

El patrón de recursos para eso se correlacionaría más estrechamente con /employees(La colección de todos los empleados)

¿Eso significa que debería haberlo hecho /employees/{empId}también porque, de ser así, hay dos URI para obtener el mismo recurso?

O tal vez todo el esquema debería ser aplanado, pero eso significaría que los empleados son un objeto anidado de nivel superior.

En un nivel básico, /employees/?company={companyId}&department={deptId}devuelve exactamente la misma vista de los empleados que el patrón más profundamente anidado.

¿Cuál es la mejor práctica para los patrones de URL donde los recursos son propiedad de otros recursos pero deberían poder consultarse por separado?


1
Este es casi exactamente el problema oppsite al descrito en stackoverflow.com/questions/7104578/… aunque las respuestas pueden estar relacionadas. Ambas preguntas son sobre la propiedad, pero ese ejemplo implica que el objeto de nivel superior no es el propietario.
Wes

1
Exactamente lo que me preguntaba. Para el caso de uso dado, su solución parece estar bien, pero ¿qué pasa si la relación es una agregación en lugar de una composición? Todavía luchando por descubrir cuál es la mejor práctica aquí ... Además, ¿esta solución implica solo la creación de la relación, por ejemplo, una persona existente está empleada o crea un objeto persona?
Jakob O.

Crea una persona en mi ejemplo ficticio. La razón por la que usé esos términos de dominio es un ejemplo razonablemente comprensible, aunque imita mi problema real. ¿Has revisado la pregunta vinculada que puede ser más difícil para una relación de agregación?
Wes

He dividido mi pregunta en una respuesta y una pregunta.
Wes

Respuestas:


152

Lo que has hecho es correcto. En general, puede haber muchos URI para el mismo recurso; no hay reglas que indiquen que no debe hacer eso.

Y, en general, es posible que deba acceder a los elementos directamente o como un subconjunto de otra cosa, por lo que su estructura tiene sentido para mí.

Solo porque los empleados son accesibles bajo el departamento:

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

No significa que no puedan ser accesibles bajo la compañía también:

company/{companyid}/employees

Lo que devolvería a los empleados de esa empresa. Depende de lo que necesite su cliente consumidor: para eso debe estar diseñando.

Pero espero que todos los manejadores de URL utilicen el mismo código de respaldo para satisfacer las solicitudes, de modo que no esté duplicando el código.


11
Esto señala el espíritu de RESTful, no hay reglas que digan que debes o no debes hacer si solo consideras un recurso significativo primero. Pero además, me pregunto cuál es la mejor práctica para no duplicar código en tales escenarios.
abookyun

13
@abookyun si necesita ambas rutas, entonces el código repetido del controlador entre ellas se puede abstraer a los objetos de servicio.
bgcode

Esto no tiene nada que ver con REST. RESTO no se preocupa por la forma en que la estructura de la parte de la ruta de sus URL ... todo lo que le importa es válida, es de esperar URI duraderos ...
redben

Con esta respuesta, creo que cualquier API donde los segmentos dinámicos sean todos identificadores únicos no debería necesitar manejar múltiples segmentos dinámicos ( /company/3/department/2/employees/1). Si la API proporciona formas de obtener cada recurso, entonces hacer cada una de esas solicitudes podría hacerse en una biblioteca del lado del cliente o como un punto final único que reutiliza el código.
max

1
Si bien no existe una prohibición, considero que es más elegante tener solo un camino hacia un recurso: simplifica todos los modelos mentales. También prefiero que los URI no cambien su tipo de recurso si hay algún anidamiento. por ejemplo /company/*, solo debe devolver el recurso de la empresa y no cambiar el tipo de recurso en absoluto. Nada de esto está especificado por REST, generalmente es una especificación pobre, solo una preferencia personal.
kashif

174

He probado ambas estrategias de diseño: puntos finales anidados y no anidados. He encontrado que:

  1. Si el recurso anidado tiene una clave primaria y no tiene su clave primaria principal, la estructura anidada requiere que la obtenga, aunque el sistema no la requiera realmente.

  2. Los puntos finales anidados generalmente requieren puntos finales redundantes. En otras palabras, la mayoría de las veces necesitará el punto final adicional / empleados para que pueda obtener una lista de empleados en todos los departamentos. Si tienes / empleados, ¿qué es exactamente lo que te compran / compañías / departamentos / empleados?

  3. Los puntos finales de anidación no evolucionan tan bien. Por ejemplo, es posible que no necesite buscar empleados ahora, pero podría hacerlo más tarde y si tiene una estructura anidada, no tiene más remedio que agregar otro punto final. Con un diseño no anidado, simplemente agrega más parámetros, lo cual es más simple.

  4. a veces un recurso puede tener múltiples tipos de padres. Resultando en múltiples puntos finales que devuelven el mismo recurso.

  5. los puntos finales redundantes hacen que los documentos sean más difíciles de escribir y también hacen que la API sea más difícil de aprender.

En resumen, el diseño no anidado parece permitir un esquema de punto final más flexible y simple.


24
Fue muy refrescante encontrar esta respuesta. He estado usando puntos finales anidados durante varios meses después de que me enseñaron que esa era la "forma correcta". Llegué a las mismas conclusiones que enumeró anteriormente. Mucho más fácil con un diseño no anidado.
user3344977

66
Parece enumerar algunas de las desventajas como ventajas. "Simplemente agrupar más parámetros en un único punto final" hace que la API sea más difícil de documentar y aprender, y no al revés. ;-)
Drenmi

44
No soy fanático de esta respuesta. No es necesario introducir puntos finales redundantes solo porque ha agregado un recurso anidado. Tampoco es un problema tener el mismo recurso devuelto por varios padres, siempre que esos padres sean los dueños del recurso anidado. No es un problema obtener un recurso principal para aprender a interactuar con los recursos anidados. Una buena API REST reconocible debería hacer esto.
Scottm

3
@Scottm: un inconveniente de los recursos anidados que encontré es que podría dar lugar a la devolución de datos incorrectos si los identificadores de recursos principales son incorrectos / no coinciden. Suponiendo que no haya problemas de autorización, se deja a la implementación de la API verificar que el recurso anidado sea realmente un hijo del recurso primario que se pasa. Si esta verificación no está codificada, la respuesta de la API podría ser incorrecta y provocar corrupción. ¿Cuáles son tus pensamientos?
Andy Dufresne

1
No necesita los identificadores principales intermedios si todos los recursos finales tienen identificadores únicos. Por ejemplo, para obtener el empleado por ID, tiene GET / companies / departamentos / empleados / {empId} o para obtener todos los empleados de la empresa 123, tiene GET / companies / 123 / departamentos / empleados / Mantener la ruta jerárquica hace que sea más evidente cómo puedes acceder a los recursos intermedios para filtrar / crear / modificar y, en mi opinión, ayuda con la capacidad de descubrimiento.
PaulG

77

Moví lo que hice de la pregunta a una respuesta donde es probable que más personas lo vean.

Lo que he hecho es tener los puntos finales de creación en el punto final anidado. El punto final canónico para modificar o consultar un elemento no está en el recurso anidado .

Entonces, en este ejemplo (solo enumerando los puntos finales que cambian un recurso)

  • POST /companies/ crea una nueva compañía devuelve un enlace a la compañía creada.
  • POST /companies/{companyId}/departments cuando se crea un departamento, el nuevo departamento devuelve un enlace a /departments/{departmentId}
  • PUT /departments/{departmentId} modifica un departamento
  • POST /departments/{deparmentId}/employees crea un nuevo empleado devuelve un enlace a /employees/{employeeId}

Por lo tanto, hay recursos de nivel raíz para cada una de las colecciones. Sin embargo, la creación está en el objeto propietario .


44
También se me ocurrió el mismo tipo de diseño. Creo que es intuitivo crear cosas como esta "donde pertenecen", pero aún así poder enumerarlas globalmente. Más aún cuando hay una relación donde un recurso DEBE tener un padre. Entonces, crear ese recurso globalmente no lo hace obvio, pero hacerlo en un sub-recurso como este tiene mucho sentido.
Joakim

Supongo que usaste POSTsignificado PUT, y de otra manera
Gerardo Lima

En realidad, no tenga en cuenta que no estoy usando Ids asignados previamente para la creación, ya que el servidor en este caso es responsable de devolver la identificación (en el enlace). Por lo tanto, escribir POST es correcto (no se puede obtener la misma implementación). Sin embargo, la opción cambia todo el recurso, pero todavía está disponible en la misma ubicación, así que lo PONGO. PUT vs POST es un asunto diferente y también es controvertido. Por ejemplo stackoverflow.com/questions/630453/put-vs-post-in-rest
Wes

@Wes Incluso prefiero modificar los métodos verbales para estar debajo del padre. Pero, ¿ve que se acepta bien el parámetro de consulta de paso para el recurso global? Ej: POST / departamentos con parámetro de consulta company = company-id
Ayyappa

1
@Mohamad Si cree que la otra manera es más fácil tanto para comprender como para aplicar restricciones, no dude en responder. Se trata de hacer explícito el mapeo en este caso. Podría funcionar con un parámetro, pero realmente esa es la pregunta. Cuál es la mejor manera.
Wes

35

He leído todas las respuestas anteriores, pero parece que no tienen una estrategia común. Encontré un buen artículo sobre las mejores prácticas en Design API de Microsoft Documents . Creo que deberías referirte.

En sistemas más complejos, puede ser tentador proporcionar URI que permitan a un cliente navegar a través de varios niveles de relaciones, como /customers/1/orders/99/products.Sin embargo, este nivel de complejidad puede ser difícil de mantener y es inflexible si las relaciones entre recursos cambian en el futuro. En su lugar, trate de mantener los URI relativamente simples . Una vez que una aplicación tiene una referencia a un recurso, debería ser posible utilizar esta referencia para buscar elementos relacionados con ese recurso. La consulta anterior se puede reemplazar con el URI /customers/1/orderspara encontrar todos los pedidos para el cliente 1 y luego /orders/99/productspara encontrar los productos en este orden.

.

Propina

Evite requerir URI de recursos más complejos que collection/item/collection.


3
La referencia que da es increíble junto con el punto en el que se destaca de no hacer URI complejos.
vicco

Entonces, cuando quiero crear un equipo para un usuario, ¿debería ser POST / teams (userId in thebody) o POST / users /: id / teams
coinhndp

@coinhndp Hola, debe usar POST / teams y podría obtener ID de usuario después de autorizar el token de acceso. Quiero decir, cuando creas un material necesitas un código de autorización, ¿verdad? No sé qué marco está utilizando, pero estoy seguro de que podría obtener ID de usuario en el controlador API. Por ejemplo: en la API ASP.NET, llame a RequestContext.Principal desde un método en ApiController. En Spring Secirity, SecurityContextHolder.getContext (). GetAuthentication (). GetPrincipal () te ayudará. En AWS NodeJS Lambda, eso es cognito: nombre de usuario en el objeto de encabezado.
Long Nguyen

Entonces, ¿qué hay de malo con POST / users /: id / teams. Creo que se recomienda en el documento de Microsoft que publicó anteriormente
coinhndp

@coinhndp Si creas un equipo como administrador, eso es bueno. Pero, como usuarios normales, no sé por qué necesita userId en la ruta. Supongo que tenemos user_A y user_B, ¿qué piensa si user_A podría crear un nuevo equipo para user_B si user_A llame a POST / users / user_B / teams. Por lo tanto, no es necesario pasar userId en este caso, userId podría obtener después de la autorización. Pero equipos /: id / proyectos es bueno para establecer una relación entre equipo y proyecto, por ejemplo.
Long Nguyen

10

El aspecto de sus URL no tiene nada que ver con REST. Todo vale. En realidad es un "detalle de implementación". Entonces, al igual que cómo nombras tus variables. Todo lo que tienen que ser es único y duradero.

No pierdas demasiado tiempo en esto, solo haz una elección y cúmplelo / sé constante. Por ejemplo, si vas con jerarquías, entonces lo haces para todos tus recursos. Si va con parámetros de consulta ... etc. al igual que las convenciones de nomenclatura en su código.

Porque ? Hasta donde yo sé, una API "RESTful" debe ser navegable (ya sabes ... "Hypermedia como el motor del estado de la aplicación"), por lo tanto, a un cliente API no le importa cómo son tus URL siempre que sean válido (no hay SEO, ningún humano que necesite leer esas "URL amigables", excepto que puede ser para depurar ...)

Lo bueno / comprensible que es una URL en una API REST solo es interesante para usted como desarrollador de API, no como cliente de API, como lo sería el nombre de una variable en su código.

Lo más importante es que su cliente API sepa cómo interpretar su tipo de medio. Por ejemplo, sabe que:

  • su tipo de medio tiene una propiedad de enlaces que enumera los enlaces disponibles / relacionados.
  • Cada enlace se identifica por una relación (al igual que los navegadores saben que el enlace [rel = "stylesheet"] significa que es una hoja de estilo o rel = favico es un enlace a un favicon ...)
  • y sabe lo que significan esas relaciones ("compañías" significa una lista de compañías, "buscar" significa una url con plantilla para hacer una búsqueda en una lista de recursos, "departamentos" significa departamentos del recurso actual)

A continuación se muestra un ejemplo de intercambio HTTP (los cuerpos están en yaml ya que es más fácil de escribir):

Solicitud

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

Respuesta: una lista de enlaces al recurso principal (empresas, personas, lo que sea ...)

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

Solicitud: enlace a empresas (usando el cuerpo de la respuesta anterior. Links.companies)

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

Respuesta: una lista parcial de compañías (debajo de los ítems), el recurso contiene enlaces relacionados, como un enlace para obtener las próximas dos compañías (body.links.next) y otro enlace (con plantilla) para buscar (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

Entonces, como ve si sigue los enlaces / relaciones, la forma en que estructura la parte de la ruta de sus URL no tiene ningún valor para su cliente API. Y si está comunicando la estructura de sus URL a su cliente como documentación, entonces no está haciendo REST (o al menos no Nivel 3 según el " modelo de madurez de Richardson ")


77
"Cuán agradable / comprensible es una URL en una API REST solo es interesante para usted como desarrollador de API, no como cliente de API, como lo sería el nombre de una variable en su código". ¿Por qué esto NO sería interesante? Esto es muy importante, si alguien que no sea usted también está utilizando la API. Esto es parte de la experiencia del usuario, por lo que diría que es muy importante que esto sea fácil de entender para los desarrolladores de clientes API. Hacer las cosas aún más fáciles de entender al vincular los recursos claramente es, por supuesto, una ventaja (nivel 3 en la url que proporcione). Todo debe ser intuitivo y lógico con relaciones claras.
Joakim

1
@Joakim Si está haciendo una API de descanso de nivel 3 (Hipertexto como el motor del estado de la aplicación), la estructura de ruta de la url no tiene ningún interés para el cliente (siempre que sea válida). Si no está apuntando al nivel 3, entonces sí, es importante y debe ser adivinable. Pero REST real es el nivel 3. Un buen artículo: martinfowler.com/articles/richardsonMaturityModel.html
redben

44
Me opongo a crear una API o interfaz de usuario que no sea fácil de usar para los seres humanos. Nivel 3 o no, estoy de acuerdo en vincular recursos es una gran idea. Pero sugerir que hacerlo "hace posible cambiar el esquema de URL" es estar fuera de contacto con la realidad y la forma en que las personas usan las API. Entonces es una mala recomendación. Pero seguro que en el mejor de los mundos todos estarían en el Nivel 3 REST. Incorporo hipervínculos Y uso un esquema de URL entendible humanamente. El nivel 3 no excluye el primero, y en mi opinión, DEBERÍA importarme. Buen artículo :) :)
Joakim

Por supuesto, uno debe preocuparse por la facilidad de mantenimiento y otras preocupaciones, creo que se pierde el punto de mi respuesta: la forma en que se ve la url no merece mucha reflexión y debe "simplemente hacer una elección y atenerse a ella / ser consistente "como dije en la respuesta. Y en el caso de una API REST, al menos mi opinión, la facilidad de uso no está en la url, está principalmente en (el tipo de medio) De todos modos, espero que entiendas mi punto :)
redben

9

No estoy de acuerdo con este tipo de camino

GET /companies/{companyId}/departments

Si desea obtener departamentos, creo que es mejor usar un recurso / departamentos

GET /departments?companyId=123

Supongo que tiene una companiestabla y una departmentstabla, luego clases para mapearlas en el lenguaje de programación que usa. También supongo que los departamentos podrían estar vinculados a otras entidades que no sean empresas, por lo que un recurso / departamentos es sencillo, es conveniente tener recursos asignados a tablas y tampoco necesita tantos puntos finales ya que puede reutilizar

GET /departments?companyId=123

para cualquier tipo de búsqueda, por ejemplo

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

Si desea crear un departamento, el

POST /departments

se debe utilizar el recurso y el cuerpo de la solicitud debe contener el ID de la empresa (si el departamento puede vincularse a una sola empresa)


1
Para mí, este es un enfoque aceptable solo si el objeto anidado tiene sentido como un objeto atómico. Si no lo son, realmente no tendría sentido separarlos.
Simme

Esto es lo que dije, si también desea poder recuperar departamentos, es decir, si usará un punto final / departamentos.
Maxime Laval

2
También puede tener sentido permitir que los departamentos se incluyan a través de la carga diferida al buscar una empresa, por ejemplo GET /companies/{companyId}?include=departments, ya que esto permite que tanto la empresa como sus departamentos se recuperen en una sola solicitud HTTP. Fractal hace esto realmente bien.
Matthew Daly

1
Cuando esté configurando acls, probablemente desee restringir el /departmentspunto final para que solo sea accesible por un administrador, y que cada empresa acceda a sus propios departamentos solo a través de `/ companies / {companyId} / departamentos`
Cuzox

@MatthewDaly OData también lo hace muy bien con $ expand
Rob Grant

1

Rails proporciona una solución a esto: anidamiento superficial .

Creo que esto es bueno porque cuando se trata directamente con un recurso conocido, no hay necesidad de usar rutas anidadas, como se ha discutido en otras respuestas aquí.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.