Quais são as práticas recomendadas para recursos aninhados REST?

301

Tanto quanto posso dizer, cada recurso individual deve ter apenas um caminho canônico . Portanto, no exemplo a seguir, quais seriam os bons padrões de URL?

Tomemos como exemplo uma representação restante das empresas. Neste exemplo hipotético, cada empresa possui 0 ou mais departamentos e cada departamento possui 0 ou mais funcionários.

Um departamento não pode existir sem uma empresa associada.

Um funcionário não pode existir sem um departamento associado.

Agora eu consideraria a representação natural dos padrões de recursos.

  • /companies Uma coleção de empresas - aceita colocar para uma nova empresa. Obter para toda a coleção.
  • /companies/{companyId}Uma empresa individual. Aceita GET, PUT e DELETE
  • /companies/{companyId}/departmentsAceita POST para um novo item. (Cria um departamento dentro da empresa.)
  • /companies/{companyId}/departments/{departmentId}/
  • /companies/{companyId}/departments/{departmentId}/employees
  • /companies/{companyId}/departments/{departmentId}/employees/{empId}

Dadas as restrições, em cada uma das seções, sinto que isso faz sentido se um pouco profundamente aninhado.

No entanto, minha dificuldade surge se eu quiser listar ( GET) todos os funcionários em todas as empresas.

O padrão de recursos para o qual seria mais próximo /employees(A coleção de todos os funcionários)

Isso significa que eu deveria ter /employees/{empId}também porque, nesse caso, existem dois URIs para obter o mesmo recurso?

Ou talvez o esquema inteiro deva ser achatado, mas isso significa que os funcionários são um objeto de nível superior aninhado.

Em um nível básico, /employees/?company={companyId}&department={deptId}retorna exatamente a mesma visão dos funcionários que o padrão mais profundamente aninhado.

Qual é a melhor prática para padrões de URL em que os recursos pertencem a outros recursos, mas devem ser consultados separadamente?

Wes
fonte
1
Esse é quase exatamente o problema do oposto ao descrito em stackoverflow.com/questions/7104578/…, embora as respostas possam estar relacionadas. Ambas as perguntas são sobre propriedade, mas esse exemplo implica que o objeto de nível superior não é o proprietário.
Wes
1
Exatamente o que eu estava pensando. Para o caso de uso especificado, sua solução parece boa, mas e se a relação for uma agregação e não uma composição? Ainda lutando para descobrir qual é a melhor prática aqui ... Além disso, essa solução implica apenas a criação do relacionamento, por exemplo, uma pessoa existente é empregada ou cria um objeto de pessoa?
Jakob O.
Cria uma pessoa no meu exemplo fictício. A razão pela qual usei esses termos de domínio é um exemplo razoavelmente compreensível, embora imitando meu problema real. Você já examinou a questão vinculada que pode ajudá-lo mais a ter um relacionamento de agressão.
8263 Wes
Dividi minha pergunta em uma resposta e uma pergunta.
Wes

Respostas:

152

O que você fez está correto. Em geral, pode haver muitos URIs para o mesmo recurso - não há regras que digam que você não deve fazer isso.

E geralmente, você pode precisar acessar itens diretamente ou como um subconjunto de outra coisa - para que sua estrutura faça sentido para mim.

Só porque os funcionários estão acessíveis no departamento:

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

Não significa que eles também não possam ser acessíveis na empresa:

company/{companyid}/employees

O que retornaria funcionários para essa empresa. Depende do que é necessário para o seu cliente consumidor - é para isso que você deve projetar.

Mas espero que todos os manipuladores de URLs usem o mesmo código de suporte para atender às solicitações, para que você não esteja duplicando o código.

Jeremyh
fonte
11
Isso está apontando o espírito do RESTful; não há regras que digam que você deva ou não deve fazer se considerar primeiro um recurso significativo . Além disso, gostaria de saber qual é a melhor prática para não duplicar código em tais cenários.
abookyun
13
@abookyun se você precisar das duas rotas, o código do controlador repetido entre elas poderá ser abstraído para objetos de serviço.
Bgcode
Isso não tem nada a ver com o REST. O descanso não se importa como você estrutura a parte do caminho de seus URLs ... tudo o que preocupa é válida, espero URIs duráveis ...
redben
Dirigindo a essa resposta, acho que qualquer API onde os segmentos dinâmicos são todos identificadores exclusivos não precisa lidar com vários segmentos dinâmicos ( /company/3/department/2/employees/1). Se a API fornecer maneiras de obter cada recurso, a realização de cada uma dessas solicitações poderá ser feita em uma biblioteca do lado do cliente ou como um terminal único que reutilize o código.
máximo
1
Embora não haja proibição, considero mais elegante ter apenas um caminho para um recurso - mantém todos os modelos mentais mais simples. Também prefiro que os URIs não alterem seu tipo de recurso se houver algum aninhamento. por exemplo, /company/*deve retornar apenas o recurso da empresa e não alterar o tipo de recurso. Nada disso é especificado pelo REST - geralmente é mal especificado - apenas preferência pessoal.
Kashif #
174

Eu tentei as duas estratégias de design - pontos de extremidade aninhados e não aninhados. Eu descobri que:

  1. se o recurso aninhado tiver uma chave primária e você não tiver sua chave primária pai, a estrutura aninhada exigirá que você a obtenha, mesmo que o sistema não a exija.

  2. pontos de extremidade aninhados geralmente requerem pontos de extremidade redundantes. Em outras palavras, você precisará mais frequentemente do ponto de extremidade / funcionários para poder obter uma lista de funcionários entre os departamentos. Se você possui / funcionários, o que exatamente / empresas / departamentos / funcionários compram para você?

  3. pontos finais de aninhamento não evoluem tão bem. Por exemplo, talvez você não precise procurar funcionários agora, mas pode precisar mais tarde e, se tiver uma estrutura aninhada, não terá outra opção a não ser adicionar outro ponto de extremidade. Com um design não aninhado, você apenas adiciona mais parâmetros, o que é mais simples.

  4. Às vezes, um recurso pode ter vários tipos de pais. Resultando em vários pontos de extremidade, todos retornando o mesmo recurso.

  5. pontos finais redundantes tornam os documentos mais difíceis de escrever e também tornam a API mais difícil de aprender.

Em resumo, o design não aninhado parece permitir um esquema de terminal mais flexível e mais simples.

Patc
fonte
24
Foi muito refrescante encontrar essa resposta. Uso vários pontos de extremidade aninhados há vários meses, depois de aprender que esse era o "caminho certo". Cheguei às mesmas conclusões que você listou acima. Muito mais fácil com um design não aninhado.
user3344977
6
Você parece listar algumas desvantagens como vantagens. "Basta colocar mais parâmetros em um único ponto final" torna a API mais difícil de documentar e aprender, e não o contrário. ;-)
Drenmi
4
Não é um fã desta resposta. Não é necessário introduzir pontos de extremidade redundantes apenas porque você adicionou um recurso aninhado. Também não é um problema ter o mesmo recurso retornado por vários pais, desde que esses pais sejam realmente donos do recurso aninhado. Não é um problema obter um recurso pai para aprender a interagir com os recursos aninhados. Uma boa API REST detectável deve fazer isso.
scottm
3
@ Scottm - Uma desvantagem dos recursos aninhados que me deparei é que ele pode levar ao retorno de dados incorretos se os IDs dos recursos pai estiverem incorretos / não correspondem. Supondo que não haja problemas de autorização, resta à implementação da API verificar se o recurso aninhado é realmente um filho do recurso pai que é passado. Se essa verificação não estiver codificada, a resposta da API poderá estar incorreta, causando corrupção. Quais são seus pensamentos?
Andy Dufresne
1
Você não precisa dos IDs intermediários se todos os recursos finais tiverem IDs exclusivos. Por exemplo, para obter o funcionário por ID, você tem GET / companies / departamentos / funcionários / {empId} ou para obter todos os funcionários da empresa 123, você tem GET / companies / 123 / departamentos / funcionários / Manter o caminho hierárquico torna mais evidente como você pode acessar os recursos intermediários para filtrar / criar / modificar e ajuda na descoberta em minha opinião.
PaulG
77

Mudei o que fiz da pergunta para uma resposta em que mais pessoas provavelmente o verão.

O que fiz foi ter os pontos de extremidade de criação no ponto de extremidade aninhado. O ponto de extremidade canônico para modificar ou consultar um item não está no recurso aninhado .

Portanto, neste exemplo (apenas listando os pontos de extremidade que alteram um recurso)

  • POST /companies/ cria uma nova empresa retorna um link para a empresa criada.
  • POST /companies/{companyId}/departments quando um departamento é criado, o novo departamento retorna um link para /departments/{departmentId}
  • PUT /departments/{departmentId} modifica um departamento
  • POST /departments/{deparmentId}/employees cria um novo funcionário retorna um link para /employees/{employeeId}

Portanto, existem recursos no nível raiz para cada uma das coleções. No entanto, a criação está no objeto proprietário .

Wes
fonte
4
Eu criei o mesmo tipo de design também. Eu acho que é intuitivo criar coisas como "onde elas pertencem", mas ainda assim poder listá-las globalmente. Ainda mais quando há um relacionamento em que um recurso DEVE ter um pai. Então, criar esse recurso globalmente não torna isso óbvio, mas fazê-lo em um sub-recurso como esse faz todo o sentido.
Joakim
Eu acho que você usou POSTsignificado PUT, e caso contrário.
Gerardo Lima
Na verdade, não observe que não estou usando IDs pré-atribuídos para criação, pois o servidor neste caso é responsável por retornar o ID (no link). Portanto, escrever POST está correto (não é possível obter a mesma implementação). O put, no entanto, altera todo o recurso, mas ainda está disponível no mesmo local, então eu o coloco. PUT vs POST é uma questão diferente e também é controversa. Por exemplo stackoverflow.com/questions/630453/put-vs-post-in-rest
Wes
@ Wes Mesmo eu prefiro modificar métodos verbais para estar sob o pai. Mas você vê que a passagem do parâmetro de consulta para o recurso global é bem aceita? Ex: POST / departamentos com consulta empresa parâmetro = empresa-id
Ayyappa
1
@ Mohamad Se você acha que o outro caminho é mais fácil, tanto na compreensão quanto na aplicação de restrições, sinta-se à vontade para responder. É sobre tornar o mapeamento explícito neste caso. Poderia funcionar com um parâmetro, mas é realmente esse o problema. Qual é a melhor maneira.
Wes
35

Eu li todas as respostas acima, mas parece que elas não têm uma estratégia comum. Encontrei um bom artigo sobre as práticas recomendadas na Design API da Microsoft Documents . Eu acho que você deveria se referir.

Em sistemas mais complexos, pode ser tentador fornecer URIs que permitem que um cliente navegue por vários níveis de relacionamentos, como /customers/1/orders/99/products.No entanto, esse nível de complexidade pode ser difícil de manter e é inflexível se os relacionamentos entre recursos mudarem no futuro. Em vez disso, tente manter os URIs relativamente simples . Depois que um aplicativo tem uma referência a um recurso, deve ser possível usá-lo para encontrar itens relacionados a esse recurso. A consulta anterior pode ser substituída pelo URI /customers/1/orderspara localizar todos os pedidos do cliente 1 e, em seguida, /orders/99/productsencontrar os produtos nesse pedido.

.

Dica

Evite exigir URIs de recursos mais complexos que collection/item/collection.

Long Nguyen
fonte
3
A referência que você fornece é incrível, juntamente com o ponto em que você se destaca por não criar URIs complexos.
Vicco
Então, quando eu quero criar uma equipe para um usuário, ele deve ser POST / equipes (userId em thebody) ou POST / usuários /: id / equipas
coinhndp
@coinhndp Olá, você deve usar o POST / teams e poderá obter o userId após autorizar o token de acesso. Quero dizer, quando você cria um material, precisa de código de autorização, certo? Não sei qual estrutura você está usando, mas tenho certeza de que você poderia obter o userId no controlador da API. Por exemplo: Na API do ASP.NET, chame RequestContext.Principal de dentro de um método no ApiController. No Spring Secirity, SecurityContextHolder.getContext (). GetAuthentication (). GetPrincipal () o ajudará. No AWS NodeJS Lambda, isso é cognito: nome de usuário no objeto de cabeçalhos.
Long Nguyen
Então, o que há de errado com o POST / users /: id / teams. Eu acho que é recomendado no documento da Microsoft que você postou acima
coinhndp
@coinhndp Se você criar uma equipe como administrador, isso é bom. Mas, como usuários normais, não sei por que você precisa de userId no caminho? Suponho que tenhamos user_A e user_B, o que você acha se user_A pudesse criar uma nova equipe para user_B se user_A chamar POST / users / user_B / teams. Portanto, não há necessidade de passar userId nesse caso, o userId pode ser obtido após a autorização. Mas equipes /: id / projects é bom para fazer uma relação entre equipe e projeto, por exemplo.
Long Nguyen
10

A aparência dos seus URLs não tem nada a ver com o REST. Qualquer coisa serve. Na verdade, é um "detalhe de implementação". Assim como você nomeia suas variáveis. Tudo o que precisam ser únicos e duráveis.

Não perca muito tempo com isso, basta fazer uma escolha e cumpri-lo / ser consistente. Por exemplo, se você segue hierarquias, o faz para todos os seus recursos. Se você usar parâmetros de consulta ... etc, assim como as convenções de nomenclatura no seu código.

Por quê então ? Tanto quanto eu sei que uma API "RESTful" deve ser navegável (você sabe ... "Hipermídia como o mecanismo do estado do aplicativo"), portanto, um cliente de API não se importa com o que são seus URLs, desde que sejam válido (não há SEO, nenhum ser humano que precise ler esses "URLs amigáveis", exceto para depuração ...)

O quão agradável / compreensível é um URL em uma API REST é interessante apenas para você como desenvolvedor da API, não como cliente da API, como seria o nome de uma variável em seu código.

O mais importante é que seu cliente de API saiba como interpretar seu tipo de mídia. Por exemplo, ele sabe que:

  • seu tipo de mídia possui uma propriedade de links que lista os links disponíveis / relacionados.
  • Cada link é identificado por um relacionamento (assim como os navegadores sabem que link [rel = "stylesheet"] significa que é uma folha de estilos ou rel = favico é um link para um favicon ...)
  • e sabe o que significam esses relacionamentos ("empresas" significa uma lista de empresas, "pesquisa" significa um URL modelo para fazer uma pesquisa em uma lista de recursos, "departamentos" significa departamentos do recurso atual)

Abaixo está um exemplo de troca HTTP (os corpos estão no yaml, pois é mais fácil escrever):

Solicitação

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

Resposta: uma lista de links para o recurso principal (empresas, pessoas, o que for ...)

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

Pedido: link para empresas (usando body.links.companies da resposta anterior)

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

Resposta: uma lista parcial de empresas (nos itens), o recurso contém links relacionados, como o link para obter as próximas empresas (body.links.next) e outro (modelo) para a pesquisa (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

Então, como você vê se segue o caminho dos links / relações, como você estrutura a parte do caminho dos seus URLs não tem valor para o seu cliente de API. E se você está comunicando a estrutura de seus URLs ao cliente como documentação, não está fazendo REST (ou pelo menos não está no nível 3, conforme " Modelo de maturidade de Richardson ")

redben
fonte
7
"O quão agradável / compreensível é um URL em uma API REST só é interessante para você como desenvolvedor da API, não como cliente da API, como seria o nome de uma variável em seu código." Por que isso NÃO seria interessante? Isso é muito importante, se alguém além de você também estiver usando a API. Isso faz parte da experiência do usuário, então eu diria que é muito importante que isso seja fácil de entender para os desenvolvedores de clientes da API. Tornar as coisas ainda mais fáceis de entender, vinculando claramente os recursos, é obviamente um bônus (nível 3 no URL que você fornece). Tudo deve ser intuitivo e lógico, com relações claras.
Joakim
1
@ Joakim Se você estiver criando uma API de descanso de nível 3 (hipertexto como o mecanismo do estado do aplicativo), a estrutura do caminho da URL não interessa absolutamente ao cliente (desde que válida). Se você não está buscando o nível 3, então sim, é importante e deve ser adivinhado. Mas o REST real é o nível 3. Um bom artigo: martinfowler.com/articles/richardsonMaturityModel.html
redben 11/11/16
4
Eu me oponho a criar uma API ou interface do usuário que não seja amigável ao ser humano. Nível 3 ou não, concordo que vincular recursos é uma ótima idéia. Mas sugerir fazer isso "torna possível alterar o esquema de URL" é estar fora de contato com a realidade e como as pessoas usam APIs. Portanto, é uma má recomendação. Mas com certeza, no melhor de todos os mundos, todos estariam no nível 3 REST. Eu incorporo hiperlinks E uso um esquema de URL humanamente compreensível. Nível 3 não exclui o primeiro, e um deve se importar na minha opinião. Bom artigo embora :)
Joakim
Naturalmente, deve-se cuidar da manutenção e de outras preocupações, acho que você perdeu o ponto da minha resposta: a aparência da URL não merece muito pensamento e você deve "apenas fazer uma escolha e cumpri-la / ser consistente "como eu disse na resposta. E, no caso de uma API REST, pelo menos a minha opinião, friendlyness usuário não está no url, é sobretudo na (o tipo de mídia) De qualquer forma eu espero que você entenda o meu ponto :)
redben
9

Eu discordo deste tipo de caminho

GET /companies/{companyId}/departments

Se você deseja obter departamentos, acho melhor usar um recurso / departamentos

GET /departments?companyId=123

Suponho que você tenha uma companiestabela e uma departmentstabela e depois classes para mapeá-las na linguagem de programação usada. Também presumo que os departamentos possam ser anexados a outras entidades que não as empresas; portanto, um recurso / departamentos é direto, é conveniente ter recursos mapeados para tabelas e você também não precisa de muitos pontos de extremidade, pois pode reutilizar

GET /departments?companyId=123

para qualquer tipo de pesquisa, por exemplo

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

Se você deseja criar um departamento, o

POST /departments

o recurso deve ser usado e o corpo da solicitação deve conter o ID da empresa (se o departamento puder ser vinculado a apenas uma empresa).

Maxime Laval
fonte
1
Para mim, essa é uma abordagem aceitável apenas se o objeto aninhado fizer sentido como um objeto atômico. Se não estiverem, não faria realmente sentido separá-las.
Simme 26/09/16
Foi o que eu disse, se você também deseja recuperar departamentos, ou seja, se usará um ponto de extremidade / departamentos.
Maxime Laval
2
Também pode fazer sentido permitir que os departamentos sejam incluídos por meio de carregamento lento ao buscar uma empresa, por exemplo GET /companies/{companyId}?include=departments, uma vez que isso permite que a empresa e seus departamentos sejam buscados em uma única solicitação HTTP. O Fractal faz isso muito bem.
Matthew Daly
1
Quando você está configurando acls, provavelmente deseja restringir o /departmentsnó de extremidade para ser acessível apenas por um administrador, e cada empresa acessa seus próprios departamentos apenas através de `/ companies / {companyId} / departamentos`
Cuzox
O @MatthewDaly OData também faz isso muito bem com $ expand #
Rob Grant
1

O Rails fornece uma solução para isso: aninhamento superficial .

Eu acho que isso é bom porque, quando você lida diretamente com um recurso conhecido, não há necessidade de usar rotas aninhadas, conforme discutido em outras respostas aqui.

partydrone
fonte