Conceitos da API REST

10

Tenho três perguntas sobre o design da API REST que espero que alguém possa esclarecer. Pesquisei incansavelmente por muitas horas, mas não encontrei respostas para minhas perguntas em nenhum lugar (talvez eu simplesmente não saiba o que procurar?).

Questão 1

Minha primeira pergunta tem a ver com ações / RPC. Desenvolvo uma API REST há algum tempo e estou acostumado a pensar nas coisas em termos de coleções e recursos. No entanto, deparei-me com alguns casos em que o paradigma parece não se aplicar e estou me perguntando se existe uma maneira de conciliar isso com o paradigma REST.

Especificamente, tenho um caso em que a modificação de um recurso faz com que um email seja gerado. No entanto, posteriormente, o usuário pode indicar especificamente que deseja reenviar o email enviado anteriormente. Ao reenviar o email, nenhum recurso é modificado. Nenhum estado é alterado. É simplesmente uma ação que precisa ocorrer. A ação está vinculada ao tipo de recurso específico.

É apropriado misturar algum tipo de chamada de ação com um URI de recurso (por exemplo /collection/123?action=resendEmail)? Seria melhor especificar a ação e passar o ID do recurso para ela (por exemplo /collection/resendEmail?id=123)? Esta é a maneira errada de fazer isso? Tradicionalmente (pelo menos com HTTP), a ação que está sendo executada é o método de solicitação (GET, POST, PUT, DELETE), mas eles realmente não permitem ações personalizadas com um recurso.

Questão 2

Eu uso a parte da URL da querystring para filtrar o conjunto de recursos retornados ao consultar uma coleção (por exemplo /collection?someField=someval). Dentro do meu controlador de API, eu determino que tipo de comparação ele fará com esse campo e valor. Eu descobri que isso realmente não funciona. Eu preciso de uma maneira de permitir que o usuário da API especifique o tipo de comparação que deseja executar.

A melhor idéia que tive até agora é permitir que o usuário da API o especifique como um apêndice ao nome do campo (por exemplo /collection?someField:gte=someval- para indicar que ele deve retornar recursos onde someFieldé maior ou igual a (> =) o que somevalfor Essa é uma boa idéia? Uma má idéia? Se sim, por quê? Existe uma maneira melhor de permitir que o usuário especifique o tipo de comparação a ser executado com o campo e valor fornecidos?

Questão 3

Costumo ver URIs que se parecem /person/123/dogscom obter os persons dogs. Geralmente evito algo assim porque, no final, acho que, ao criar um URI como esse, você está apenas acessando uma dogscoleção filtrada por um personID específico . Seria equivalente a /dogs?person=123. Existe realmente uma boa razão para um URI REST ter mais de dois níveis de profundidade ( /collection/resource_id)?

Justin Warkentin
fonte
10
Você tem três perguntas. Por que não publicá-los separadamente?
Anaximander
3
Seria melhor dividir isso em três perguntas separadas. Um espectador pode conseguir uma excelente resposta para uma, mas não para todas as perguntas.
2
Eu acho que todos eles estão relacionados. O título é um pouco de alto nível, mas esta pergunta ajudará muitas pessoas e é facilmente encontrada durante uma pesquisa SE. Esta questão deve se tornar Wiki da comunidade assim que forem adicionados votos e substância suficientes. Levei semanas para pesquisar essas coisas.
Andrew T Finnell
1
Pode ter sido melhor publicá-las separadamente, IDK. No entanto, como o @AndrewFinnell mencionou, achei que seria uma boa ideia manter as perguntas juntas, pois essas foram as perguntas mais difíceis relacionadas ao REST que já tive e seria bom que outras pessoas encontrassem as respostas. juntos.
11113 Justin Warkentin

Respostas:

11

É apropriado misturar algum tipo de chamada de ação com um URI de recurso (por exemplo /collection/123?action=resendEmail)? Seria melhor especificar a ação e passar o ID do recurso para ela (por exemplo /collection/resendEmail?id=123)? Esta é a maneira errada de fazer isso? Tradicionalmente (pelo menos com HTTP), a ação que está sendo executada é o método de solicitação (GET, POST, PUT, DELETE), mas eles realmente não permitem ações personalizadas com um recurso.

Prefiro modelar isso de uma maneira diferente, com uma coleção de recursos representando os emails que devem ser enviados; o envio será processado pelos internos do serviço em devido tempo, quando o recurso correspondente será removido. (Ou o usuário pode EXCLUIR o recurso mais cedo, causando o cancelamento da solicitação para enviar.)

Faça o que fizer, não coloque verbos no nome do recurso! Esse é o substantivo (e a parte da consulta é o conjunto de adjetivos). Verbos verbais esquisitos REST!

Eu uso a parte da URL da querystring para filtrar o conjunto de recursos retornados ao consultar uma coleção (por exemplo /collection?someField=someval). Dentro do meu controlador de API, eu determino que tipo de comparação ele fará com esse campo e valor. Eu descobri que isso realmente não funciona. Eu preciso de uma maneira de permitir que o usuário da API especifique o tipo de comparação que deseja executar.

A melhor ideia que me foi apresentada até agora é permitir que o usuário da API o especifique como um apêndice ao nome do campo (por exemplo /collection?someField:gte=someval- para indicar que ele deve retornar recursos onde someField é maior ou igual a ( >=) o que somevalfor. É uma boa ideia? Uma má idéia? Se sim, por quê? Existe uma maneira melhor de permitir que o usuário especifique o tipo de comparação a ser executado com o campo e valor fornecidos?

Prefiro especificar uma cláusula de filtro geral e tê-la como um parâmetro de consulta opcional em qualquer solicitação para buscar o conteúdo da coleção. O cliente pode especificar exatamente como restringir o conjunto retornado, da maneira que desejar. Eu também me preocuparia um pouco com a descoberta da linguagem de filtro / consulta; quanto mais rico, mais difícil é descobrir clientes arbitrários. Uma abordagem alternativa que, pelo menos teoricamente, lida com esse problema de descoberta é permitir a criação de sub-recursos de restrição da coleção, que os clientes obtêm ao postar um documento descrevendo a restrição ao recurso de coleção. Ainda é um abuso leve, mas pelo menos é um que você pode claramente tornar descoberto!

Esse tipo de descoberta é uma das coisas que acho menos forte com o REST.

Costumo ver URIs que parecem algo /person/123/dogspara levar as pessoas a cães. Geralmente, evito algo assim porque, no final, acho que, ao criar um URI como esse, você está apenas acessando uma coleção de cães filtrada por um ID de pessoa específico. Seria equivalente a /dogs?person=123. Existe realmente uma boa razão para um URI REST ter mais de dois níveis de profundidade ( /collection/resource_id)?

Quando a coleção aninhada é realmente um sub-recurso das entidades membros da coleção externa, é razoável estruturá-las como um sub-recurso. Por "sub-recurso", quero dizer algo como a relação de composição UML, onde destruir o recurso externo naturalmente significa destruir a coleção interna.

Outros tipos de coleção podem ser modelados como um redirecionamento HTTP; portanto, /person/123/dogspode realmente ser respondido fazendo um 307 que redireciona para /dogs?person=123. Nesse caso, a coleção não é realmente composição UML, mas agregação UML. A diferença importa; é significativo!

Donal Fellows
fonte
2
Você tem pontos sólidos no geral. No entanto, embora a resendEmailação possa ser tratada com a criação de uma coleção e o POST, ela parece menos natural. Na verdade, não guardo nada no banco de dados quando um e-mail é reenviado (sem necessidade). Nenhum recurso é modificado; portanto, é simplesmente uma ação que é bem-sucedida ou falha. Não pude retornar um ID de recurso existente além da vida útil da chamada, tornando essa implementação um hack em vez de ser RESTful. Simplesmente não é uma operação CRUD.
11113 Justin Warkentin
3

É compreensível ficar um pouco confuso sobre como usar corretamente o REST com base em todas as maneiras pelas quais grandes empresas projetam suas APIs REST.

Você está certo de que o REST é um sistema de Coleta de Recursos. Significa Representational State Transfer. Não é uma ótima definição se você me perguntar. Mas os principais conceitos são os 4 VERBs HTTP e são sem estado.

O ponto importante a ser observado é que você só possui 4 VERBOS com REST. Estes são GET, POST, PUT e DELETE. Seu resendexemplo seria adicionar um novo verbo ao REST. Isso deve ser uma bandeira vermelha.

Questão 1

É importante perceber que o responsável pela chamada da API REST não deve saber que executar um PUTem sua coleção resultaria na geração de um email. Isso cheira a um vazamento para mim. O que eles poderiam saber é que executar um PUTpoderia resultar em tarefas extras que eles poderiam consultar posteriormente. Eles saberiam disso executando um GETrecurso recentemente criado. Isso GETretornaria o recurso e todos os TaskIDs de recurso associados a ele. Posteriormente, você poderá consultar essas tarefas para determinar seu status e até enviar um novo Task.

Você tem poucas opções.

REST - Abordagem baseada em recursos de tarefas

Crie um tasksrecurso no qual você possa enviar tarefas específicas ao seu sistema para executar ações. Você pode executar GETa tarefa com base na IDdevolução para determinar seu status.

Ou você pode misturar um SOAP over HTTPserviço da Web para adicionar algum RPC à sua arquitetura.

consultando todas as tarefas para um recurso específico

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

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

exemplo de recurso de tarefa

PUT http://server/api/tasks

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

==> retorna o ID da tarefa

223334

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

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

REST - Usando o POST para acionar ações

Você sempre pode POSTdados adicionais para um recurso. Na minha opinião, isso violaria o espírito do REST, mas ainda seria compatível.

Você pode fazer um POST semelhante a este:

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

{ "action" : "send-email" }

Você estará atualizando o recurso 123 da coleção com dados adicionais. Esses dados adicionais são essencialmente uma ação que solicita ao back-end que envie um email para esse recurso.

O problema que tenho com isso é que um GETrecurso retornará esses dados atualizados. No entanto, isso resolveria seus requisitos e ainda seria RESTful.

SOAP - Serviço da Web que aceita recursos obtidos do REST

Crie um novo WebService no qual você pode enviar emails com base no ID do recurso anterior da API REST. Não entrarei em detalhes sobre o SOAP aqui, pois a pergunta original é sobre o REST e esses dois conceitos / tecnologias não devem ser comparados, pois são maçãs e laranjas .

Questão 2

Você também tem algumas opções aqui:

Parece que muitas empresas maiores que publicam APIs REST expõem uma searchcoleção que é realmente apenas uma maneira de passar parâmetros de consulta para retornar recursos.

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

O que retornaria uma coleção de recursos REST totalmente qualificados, como:

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

Ou você pode permitir algo como MVEL como um parâmetro de consulta.

Questão 3

Eu prefiro os subníveis do que ter que voltar e consultar o outro recurso com um parâmetro de consulta. Não acredito que exista alguma regra de uma maneira ou de outra. Você pode implementar os dois lados e permitir que o chamador decida qual é o mais apropriado com base em como eles entraram no sistema pela primeira vez.

Notas

Discordo dos comentários de legibilidade de outras pessoas. Apesar do que alguns possam pensar, o REST ainda não é para consumo humano. É para consumo da máquina. Se eu quiser ver meus Tweets, uso o site regular do Twitters. Eu não realizo um REST GET com sua API. Se eu quiser fazer algo programaticamente com meus tweets, utilizarei a API REST. Sim, as APIs devem ser compreensíveis, mas você gtenão é tão ruim, apenas não é intuitivo.

A outra coisa principal com o REST é que você poderá iniciar em qualquer ponto da sua API e navegar para todos os outros recursos associados, SEM saber o URL exato dos outros recursos antes do tempo. Os resultados do GETVERBO no REST devem retornar a URL REST completa dos recursos que referencia. Portanto, em vez de uma consulta retornar o ID de um Personobjeto, ele retornaria a URL totalmente qualificada, como http://server/api/people/13. Em seguida, você sempre pode navegar pelos resultados de forma programática, mesmo que o URL tenha sido alterado.

Resposta ao comentário

No mundo real, de fato, existem coisas que precisam acontecer que não criam, leem, atualizam ou excluem (CRUD) um recurso.

Ações adicionais podem ser executadas em recursos. Os bancos de dados relacionais típicos suportam o conceito de procedimentos armazenados. Esses são comandos adicionais que podem ser executados em um conjunto de dados. O REST não possui esse conceito inerentemente. E não há razão para isso. Esses tipos de ações são perfeitos para serviços da Web RPC ou SOAP.

Esse é o problema geral que vejo nas APIs REST. Os desenvolvedores não gostam das limitações conceituais que cercam o REST e, portanto, o adaptam para fazer o que quiserem. Isso o impede de ser um serviço RESTful. Essencialmente, esses URLs são GETchamados para servlets do tipo pseudo-REST.

Você tem poucas opções:

  • Crie um recurso de tarefa
  • Apoiar POSTing dados adicionais para o recurso para executar uma ação.
  • Adicione os comandos adicionais através de um serviço da Web SOAP.

Se você usasse um parâmetro de consulta que VERB HTTP usaria para reenviar o email?

  • GET- Isso reenvia o email E retorna os dados do recurso? E se um sistema armazenasse em cache esse URL e o tratasse como o URL exclusivo desse recurso. Toda vez que eles acessam o URL, ele reenvia um e-mail.
  • POST - Na verdade, você não enviou novos dados para o recurso, apenas um parâmetro de consulta adicional.

Com base em todos os requisitos fornecidos, executar um POSTno recurso com action fielddados como POST resolverá o problema.

Andrew T Finnell
fonte
3
Enquanto o REST implementado via HTTP fornece esses 4 verbos, não estou convencido de que esses verbos devam ser o fim disso. No mundo real, de fato, existem coisas que precisam acontecer que não criam, leem, atualizam ou excluem (CRUD) um recurso. Reenviar um email é uma dessas coisas. Não preciso armazenar ou modificar nada no banco de dados. É simplesmente uma ação que consegue ou falha.
11113 Justin Warkentin
@JustinWarkentin Entendo quais são suas necessidades. Mas isso não faz do REST algo que não é. A adição de um novo verbo ao URL é contra a arquitetura REST. Atualizarei minha resposta para oferecer outra alternativa que seria RESTful.
Andrew T Finnell
@JustinWarkentin Confira 'REST - Usando o POST para acionar ações' na minha resposta.
Andrew T Finnell
0

Pergunta 1: É apropriado misturar algum tipo de chamada de ação com um URI de recurso [ou], seria melhor especificar a ação e passar o ID do recurso para ela?

Boa pergunta. Nesse caso, aconselho que você use a última abordagem, especifique a ação e passe um ID de recurso para ela. Dessa forma, quando seu recurso é modificado pela primeira vez, ele chama a /sendEmailação (observação: não há necessidade de chamá-lo de "reenviar") como uma solicitação RESTful separada (que você pode chamar posteriormente repetidamente, independentemente do recurso que está sendo modificado )

Pergunta 2: sobre como usar um operador de comparação da seguinte forma:/collection?someField:gte=someval

Embora isso seja tecnicamente correto, provavelmente é uma má idéia. Um dos princípios principais do REST é a legibilidade. Eu sugiro que você simplesmente passe o operador de comparação como outro parâmetro, por exemplo: /collection?someField=someval&operator=gtee, é claro, projete sua API para atender a um caso padrão (caso o operatorparâmetro seja deixado de fora do URI).

Pergunta 3: Existe realmente uma boa razão para um URI REST ter mais de dois níveis de profundidade?

Sim; para abstração. Eu já vi algumas APIs REST que utilizam camadas de abstração através de vários níveis de URI, por exemplo: /vehicles/cars/123ou /vehicles/bikes/123que, por sua vez, permitem que você trabalhe com informações úteis referentes a coleções /vehiclese /vehicles/bikescoleções. Dito isto, eu não sou um grande fã dessa abordagem; você raramente precisará fazer isso na prática e é provável que você possa redesenhar a API para usar apenas dois níveis.

E sim, como sugerem os comentários acima, no futuro seria melhor dividir suas perguntas em postagens separadas;)

Kosta Kontos
fonte
Eu acho que meu exemplo para a pergunta 2 foi simplista. Preciso especificar um operador de comparação para cada campo usado para filtrar a coleção, não apenas um, portanto, no seu exemplo, teria que ser algo parecido /collection?field1=someval&field1Operator=gte&field2=someval&field2Operator=eq.
23813 Justin Warkentin
0

Para a pergunta 2, uma alternativa diferente pode ser mais flexível: considere cada pesquisa um recurso que o usuário cria antes de usar.

digamos que você tenha um contêiner de "pesquisas", lá você faz um POST /api/searches/com a especificação de consulta no conteúdo. pode ser um documento JSON, XML ou mesmo SQL, o que for mais fácil para você. Se a consulta analisar corretamente, uma nova pesquisa será criada como um novo recurso com seu próprio URI, digamos/api/searches/q123/

Em seguida, o cliente pode simplesmente GET /api/searches/q123/recuperar os resultados da consulta.

Por fim, você pode solicitar ao cliente que exclua a consulta ou limpe-a após o fechamento da sessão.

Javier
fonte
0

É apropriado misturar algum tipo de chamada de ação com um URI de recurso (por exemplo, / collection / 123? Action = resendEmail)? Seria melhor especificar a ação e passar a identificação do recurso para ela (por exemplo, / collection / resendEmail? Id = 123)? Esta é a maneira errada de fazer isso? Tradicionalmente (pelo menos com HTTP), a ação que está sendo executada é o método de solicitação (GET, POST, PUT, DELETE), mas eles realmente não permitem ações personalizadas com um recurso.

Não, não é apropriado, pois os IRIs são para identificar recursos e não operações (no entanto, as pessoas usam essa abordagem de substituição de método por um tempo, nos casos em que o uso de métodos não POST e GET não é suportado). O que você pode fazer é procurar um método HTTP apropriado ou criar um novo. O POST pode ser seu amigo nesses casos (por exemplo, use-o se eles não encontrarem um método apropriado e a solicitação não for recuperada). Outra abordagem para gerar recursos com o envio de e-mails e, portanto, POST /emailspode enviar os e-mails sem criar um recurso real. Btw. A estrutura de URI não possui semântica, portanto, da perspectiva do REST, não importa realmente que tipo de URIs você usa. O que importa são os metadados (por exemplo, relação de link ) atribuídos aos links que você enviou aos clientes.

A melhor idéia que me foi apresentada até agora é permitir que o usuário da API o especifique como um apêndice ao nome do campo (por exemplo, / collection? SomeField: gte = someval - para indicar que ele deve retornar recursos onde someField é maior que ou igual a (> =) qualquer que seja o valor. Isso é uma boa idéia? Uma idéia ruim? Em caso afirmativo, por quê? Existe uma maneira melhor de permitir que o usuário especifique o tipo de comparação a ser executado com o campo e valor fornecidos?

Você não precisa criar uma linguagem de consulta própria. Prefiro usar um já existente e adicionar alguma descrição da consulta aos metadados do link. Você provavelmente deve usar um tipo de mídia RDF (por exemplo, JSON-LD) para fazer isso ou usar um tipo MIME personalizado (depois que não houver um formato não RDF que suporte isso). O uso de padrões existentes desacopla seu cliente do servidor, é disso que trata a restrição de interface uniforme.

Seria equivalente a / dogs? Person = 123. Existe realmente uma boa razão para um URI REST ter mais de dois níveis de profundidade (/ collection / resource_id)?

Como mencionei anteriormente, a estrutura do URI não importa da perspectiva REST. Você poderia usar /x71fd823df2por exemplo. Ainda faria sentido para os clientes porque eles verificam os metadados atribuídos aos links e não à estrutura do URI. O principal objetivo do URI é identificar recursos. No padrão URI, eles afirmam que o caminho contém dados hierárquicos e a consulta contém dados não hierárquicos. Mas pode ser muito subjetivo o que é hierárquico. É por isso que você atende a vários níveis de URIs e URIs profundos com consultas longas também.

Pesquisei incansavelmente por muitas horas, mas não encontrei respostas para minhas perguntas em nenhum lugar (talvez eu simplesmente não saiba o que procurar?).

Você deve ler pelo menos as restrições REST da dissertação Fielding , o padrão HTTP e provavelmente as APIs da web de terceira geração da Markus.

inf3rno
fonte