Menu

API REST - Modelagem de Recursos

“A principal abstração de uma informação em REST é um recurso. Qualquer informação que possa ser nomeada pode ser um recurso: um documento ou imagem, um serviço meteorológico (por exemplo, "o tempo de hoje em Los Angeles"), uma coleção de outros recursos, um objeto não-virtual (por exemplo, uma pessoa), e assim por diante. Em outras palavras, qualquer conceito que possa ser usado por um autor como referência em hipertexto caberá dentro da definição de um recurso. Um recurso é um mapeamento conceitual de um conjunto de entidades, e não a entidade que corresponde ao mapeamento em algum ponto específico.” - Dissertação de Roy Fielding.

Um recurso pode ser algo único ou uma coleção. Por exemplo, "clientes" é uma coleção de recursos e "cliente" é um recurso único (em um domínio bancário). Podemos identificar a coleção de recursos "clientes" usando o URN "/clientes". Podemos identificar um recurso único "cliente" usando o URN "/clientes/{idCliente}".

Um recurso também pode "conter" uma sub-coleção de recursos. Por exemplo, a sub-coleção de recursos "contas" de um "cliente" em particular, pode ser identificada usando o URN "/clientes/{idCliente}/contas" (em um domínio bancário). Da mesma forma, um recurso único "conta" dentro de uma sub-coleção de recursos "clientes" pode ser identificado como segue: "clientes/{idCliente}/contas/{idConta}".

O ponto de partida na seleção de recursos é analisar o seu domínio de negócios e extrair substantivos que são relevantes para suas necessidades. Mais importante ainda, o foco deve ser nas necessidades dos consumidores da API e como fazê-la relevante e útil do ponto de vista das interações do consumidor. Uma vez que os substantivos (recursos) forem identificados, as interações com a API podem ser modeladas com chamadas HTTP sobre estes substantivos. Quando os substantivos não mapeiam muito bem o negócio, podemos aproximar. Por exemplo, podemos facilmente usar a abordagem "substantivos do domínio" e identificar recursos básicos, como Postagem, Tag, Comentário, etc. no caso de um blog. Da mesma forma, podemos identificar os substantivos Cliente, Endereço, Conta, Contador, etc. como recursos em um domínio bancário.

Se pegarmos como exemplo o substantivo "Conta", temos alguns verbos relacionados, como "abrir" (abrir uma conta), "fechar" (fechar uma conta), "depositar" (depositar dinheiro em uma conta), "retirar" (retirar dinheiro de uma conta), etc. Esses verbos podem ser muito bem mapeados para métodos HTTP. Por exemplo, um consumidor dessa API pode "abrir" uma conta através da criação de uma instância do recurso "Conta", usando o método HTTP POST. Da mesma forma, um consumidor dessa API pode "fechar" uma conta usando o método HTTP DELETE. O consumidor pode também "retirar" ou "depositar" dinheiro utilizando os métodos HTTP "PUT", "PATCH" ou "POST". Este tutorial explica muito bem o básico sobre seleção e nomeação de recursos.

No entanto, esta abordagem simplista pode ser válida em um nível abstrato, mas não se aplica quando encontramos domínios mais complicados na prática. Eventualmente, você se depara com conceitos que não são cobertos por substantivos óbvios/habituais. As seções seguintes exploram a arte da modelagem de recursos e a importância de modelá-los com boa granularidade.

Recursos de CRUD com granularidade fina contra recursos com granularidade grossa

Se nós projetamos uma API com recursos de granularidade muito fina, acabaríamos com uma API muito prolixa para as aplicações dos consumidores. Por outro lado, se nós projetamos uma API baseada em recursos de granularidade mais grossa (ou seja, projetado para diversas finalidades), não haverá variações suficientes para suportar todas as necessidades dos consumidores da API e a mesma pode tornar-se muito difícil de usar e manter. Para entender isso melhor, vamos considerar como exemplo a modelagem de uma API para um blog:

A API para criação de uma nova postagem no blog pode ser projetada de duas maneiras. A primeira abordagem é projetar múltiplas APIs - uma para cada postagem (título e conteúdo textual), imagem e anexos, tags sobre o conteúdo ou imagem, etc. Esta abordagem faz com que a API tenha granularidade mais fina, resultando em interações mais verbosas entre o consumidor da API e provedor. Esta abordagem vai exigir que os consumidores façam várias requisições ao servidor da API. O servidor vai acabar recebendo um número significativamente maior de solicitações HTTP - possivelmente afetando sua capacidade de atender vários consumidores da API. A segunda abordagem é projetar uma API de granularidade mais grossa para a postagem em um blog (coleção de recursos "Postagens"), que pode incluir título da postagem, o conteúdo, imagens e tags. Assim, será necessário fazer apenas uma requisição para o servidor da API, reduzindo a carga no servidor.

Por outro lado, uma aplicação consumidora deve ser capaz de "curtir" uma postagem do blog, fazendo uma requisição para uma sub-coleção de recursos "Curtidas"  ("/ postagens/{postagem_id}/curtidas"). Também deve ser possível adicionar um comentário no blog, fazendo uma requisição separadamente na API para a sub-coleção de recursos "Comentários" ("/postagens/{postagem_id}/comentarios"), sem ter que passar pelo recurso "Postagem" (/postagens/{postagem_id})". Se ao invés de usar essas sub-coleções de recursos, tivéssemos usado um único recurso de granularidade mais grossa, como "Postagem" (/postagens/{postagem_id}), para "curtir" ou "comentar", tornaria o trabalho de ambos provedor e consumidor da API mais difícil. Com essa abordagem de um único recurso "Postagem", para adicionar um comentário ou curtir uma postagem no blog, o provedor da API tem de fornecer uma opção para o consumidor da API indicar que a solicitação é para adicionar um comentário, ou curtir uma postagem - o que pode ser feito através da especificação de um elemento XML em separado ou através de uma propriedade JSON nos dados a serem entregues, o que irá indicar qual o tipo de dado a ser entregue. Já no lado do servidor, o provedor da API tem de olhar para o elemento fornecido e decidir se a requisição é para adicionar um comentário ou curtir uma postagem ou até mesmo para atualizar o conteúdo da postagem, etc. O mesmo vale para atualizações dos comentários no blog. O código do lado do consumidor da API também precisa lidar com essas variações nos dados a serem entregues durante a utilização do recurso único, o que resulta em complexidade indesejada.

Prevenindo a migração da lógica de negócio para o consumidor da API

Se é esperado que os consumidores da API manipulem diretamente os recursos de baixo nível (utilizando APIs de granularidade fina), como CRUD, haverá duas grandes consequências: em primeiro lugar, as interações entre consumidor e provedor serão muito verbosas. Em segundo lugar, a lógica de negócios vai começar a transbordar para o consumidor da API. No nosso exemplo de uma API para um blog, APIs mais granulares podem deixar os dados do blog em um estado inconsistente e criar problemas de manutenção. Por exemplo, a aplicação do blog pode ter uma lógica de negócios que diz que adicionar tags sobre o conteúdo é obrigatório ou que tags de imagem podem ser adicionadas somente quando o post tem uma imagem, etc. Para fazer isso corretamente, o consumidor precisa fazer todas as solicitações necessárias à API na seqüência correta - uma requisição básica com o conteúdo da postagem, outra requisição com imagem, outra requisição com tags, etc. Se o consumidor faz uma requisição à API para criar a postagem mas não faz uma requisição para adicionar as tags, logo, o post ficará com dados inconsistentes, uma vez que as tags são obrigatórias no contexto da aplicação. Isso implica no seguinte fato: consumidor da API precisa entender e aplicar a lógica do negócio (tais como garantir que as tags estão associadas, garantindo a sequência correta de requisições à API, etc.) em seu lado.

Mesmo que os consumidores da API entendam claramente essa responsabilidade, o que acontece quando há falhas? Por exemplo, a primeira requisição à API passa com sucesso - cria o post, mas a segunda requisição para adicionar tags falha. Isso deixa os dados em um estado inconsistente. Nessa situação, deve haver um acordo muito claro sobre o que se espera do consumidor da API. O consumidor pode repetir a requisição? Se não, quem vai limpar os dados?, etc. Essa responsabilidade não é sempre bem compreendida e muito difícil de aplicar. Dado que a lógica de negócios pode sofrer alterações, essa abordagem pode aumentar os esforços de manutenção - especialmente quando existem diferentes tipos de consumidores (aplicativo móveis, web, etc.) e quando os consumidores são desconhecidos ou em maior número (para APIs públicas).

Basicamente, a abordagem CRUD coloca a lógica de negócios no código do cliente, criando forte acoplamento entre o cliente (consumidor) e o provedor (API), que não ficará responsável pela integridade, e isso faz com que o provedor perca o intuito do usuário, decompondo-o no próprio cliente. A qualquer momento a lógica de negócios pode mudar, e, logo, todos os consumidores da API terão que mudar o código e reimplantar o sistema. Isso é impossível de ser feito em alguns casos, tais como em aplicações móveis nativas, pois os clientes não estão interessados em atualizar frequentemente seus aplicativos. Além disso, disponibilizar serviços de baixo nível que suportam interações mais prolixas significa que a API será obrigada a suportar todos os serviços de baixo nível, até mesmo quando os serviços são atualizados para manter a compatibilidade com os consumidores.

No caso de APIs de granularidade mais grossa, a lógica de negócio permanece no lado do provedor da API, reduzindo assim os problemas de inconsistência de dados discutidos anteriormente. O consumidor da API não deverá ter nem mesmo conhecimento da lógica de negócios que será aplicada no servidor, e de fato não precisará de saber em muitos casos.

Nota: Quando falamos de prevenir a migração de lógica de negócios, estamos falando sobre a lógica de controle dos fluxos de negócio (por exemplo, fazer todas as requisições necessárias à API em sequência correta) e não a lógica de negócios funcional (por exemplo, cálculo de imposto).

Recursos de granularidade grossa para processos de negócio

Como podemos conciliar interfaces de granularidade grossa que representam uma capacidade de negócio com verbos HTTP e recursos nomeados? O que eu faço se eu não tenho substantivos suficientes? E se o meu serviço está lidando com múltiplos (dois ou mais) recursos e muitas operações sobre esses recursos? Como garantir interações de granularidade grossa com muitos substantivos e poucos verbos? E como podemos evitar a abordagem CRUD, de baixo nível, e falar uma linguagem mais alinhada com os termos do negócio?

Vamos retomar o que a dissertação de Roy Fielding diz sobre recurso: "...qualquer conceito de que possa ser usado por um autor como referência em hipertexto deve caber dentro da definição de um recurso...". Necessidades de negócios ou processos podem perfeitamente se encaixar na definição de recursos. Em outras palavras, para processos de negócio complexos que abrangem vários recursos, podemos considerar o próprio processo de negócio como um recurso. Por exemplo, o processo de criação de um novo cliente em um domínio bancário pode ser modelado como um recurso. O CRUD é, na verdade, apenas um processo de negócio mínimo, aplicável a praticamente qualquer recurso. Isso nos permite modelar processos de negócio como verdadeiros recursos que podem ser representados em sua essência.

É muito importante distinguir entre recursos de API REST e entidades de domínio em DDD (domain driven design). O DDD se aplica à implementação (incluindo a implementação de API), enquanto os recursos na API REST conduzem o projeto da API e o contrato. Seleção de recursos de API não deve depender de detalhes adjacentes da implementação do domínio.

Evitando CRUD

A maneira de evitar a abordagem CRUD é criar operações de negócio ou recursos que representem processos de negócio, ou o que podemos chamar de recursos "intuitivos", que expressam o "estado de querer algo" ou "estado do processo para atingir um resultado final" em um nível de negócio ou domínio. Mas para fazer isso, é preciso garantir que sejam identificados os verdadeiros proprietários de todo o seu estado. Em um mundo onde o CRUD possui quatro verbos (AtomPub-style), é como se você permitisse que partes externas aleatórias alterassem o estado do seu recurso, por meio de PUT e DELETE, como se o serviço fosse apenas um banco de dados de baixo nível. O PUT expõe muito conhecimento de domínio interno para o cliente. O consumidor não deve manipular representações internas; ele deve ser apenas uma fonte de intenções de usuário. Para entender isso melhor, vamos considerar um exemplo em que um cliente de um domínio bancário tem a intenção de mudar seu endereço. Isso pode ser feito de duas maneiras:

  1. Na primeira abordagem, uma API CRUD construída em torno do recurso "Cliente" pode ser utilizada diretamente para atualizar o endereço do cliente. Isto é, para atualizar o endereço de um cliente existente, uma solicitação HTTP PUT pode ser feita para o recurso "Cliente" (ou recurso "Endereço" se existir). Se optarmos por esta abordagem API CRUD, eventos significativos ocorridos em dados de negócio, como quando um endereço foi alterado, quem o alterou (alterado pelo cliente ou pelo funcionário do banco), qual foi a mudança, histórico de mudanças, etc, ficam esquecidos. Em outras palavras, vamos perder eventos relevantes ocorridos nos dados de negócio, que podem ser úteis posteriormente. Além disso, com essa abordagem, o código do consumidor precisa ter o conhecimento do domínio "Cliente" (incluindo atributos do cliente, etc.). Se a definição do domínio "Cliente" muda, o código do consumidor pode exigir atualizações imediatas, mesmo se o consumidor não usa os atributos afetados pela mudança. Isso faz com que o código do consumidor fique mais frágil.
  2. ​Uma abordagem alternativa que contorna o problema do CRUD, é projetar a API em torno de recursos que são baseados em processos de negócio e eventos do domínio. Por exemplo, para atualizar o endereço de um cliente existente em um banco, uma solicitação POST pode ser feita para recurso "MudancaDeEndereco". Este recurso "MudancaDeEndereco" pode capturar os dados completos do evento de mudança de endereço (como quem mudou, o que foi a mudança, etc.). Essa abordagem é especialmente útil em situações onde eventos significativos em dados de negócio serão úteis para as necessidades imediatas do negócio, (como longos processos assíncronos que fazem mudanças de endereço como parte de um processo batch, rodando em background) ou sob a perspectiva de longo prazo (como análises que mostram o histórico de mudanças, para fins de auditoria, etc.). Mesmo que não haja necessidade imediata do negócio ou previsão para manter os dados de eventos, fazendo um POST para tal recurso "intuitivo" "MudancaDeEndereco" ainda pode ser considerado (com uma análise custo-benefício), para evitar que o consumidor tenha conhecimento do domínio interno. Isso mantém o código do consumidor menos sujeito a alterações na definição do domínio "Cliente". Quando os dados de eventos não são necessários para o negócio, é opcional persistir eventos em "MudancaDeEndereco". Podemos optar por aplicar diretamente a alteração de endereço sem armazenar dados de evento em "MudancaDeEndereco".

Evitar o CRUD significa certificar-se de que o serviço que hospeda o recurso é o único agente que pode alterar diretamente o seu estado. Isso pode significar separar recursos em mais recursos de acordo com quem realmente é responsável por seu estado. Então basta apenas que todos façam o POST de suas "intenções" ou publiquem os estados do recurso que eles mesmos possuem, talvez para votação.

Substantivos contra Verbos

The argument over Nouns and Verbs is endless. Let us consider an example - setting up a new customer in a bank. This business process can be called either EnrollCustomer, or CustomerEnrollment. In this case, the term CustomerEnrollment sounds better because it is noun-ish. It also reads better: “CustomerEnrollment number 2543 for customer xxxx”. It also has the additional benefit of maintaining business relevant, independently query-able and evolving state. The business equivalent of such a resource is a typical form that we may fill out in a business, which triggers a business process. Thinking about the paper form analogy in a typical business function helps us to focus on the business requirements in a technology agnostic way as discussed by Dan North in his article “A Classic Introduction to SOA”.

A typical customer enrollment may involve sending a KYC (Know Your Customer) request to an external agency, registering the customer, creating an account, printing debit cards, sending a mail, etc. These steps may be overlapping and the process would be long-running with several failure points. This is probably a more concrete example where we may model the process as a resource. A process like this will result in creation / updates of multiple low level resources such as Customer, Account, KYCRequest, etc. A GET for such a process will make sense, because we would get back the state of the process currently.

If we don’t have the Customer enrollment process modeled as a resource, the API consumer has to then “know” the business logic that a customer enrollment involves - one request to create customer resource, one request for KYC request, one request for print card request, etc. Essentially, all your API consumers will have to understand and apply the business logic in their code. If an API consumer missed out a step such as “print card request”, then you have an incomplete enrollment and an unhappy customer because she did not receive the card. This is clearly error prone.

Perhaps this can be a rule of thumb: Does the process need state of its own? Will the business be asking questions about this process such as - what is the status of the process?  if it failed, why? Who initiated it and from where? how many of them happened? What are the most common reasons for failure of the process, and at which step? How long did it take on average, min, max? For most non-trivial processes, businesses want answers to these questions. And such a process should be modeled as a resource in its own right.

And this is where the noun-based approach starts getting limiting. Business Processes are of course behavior and the business language often focuses on the verb. But they are also "things" to the business. And given that we can convert most verbs into nouns, the distinction starts becoming blurred. And really it’s just how you want to perceive it - any noun can be verbed and vice-versa. The question is what do you want to do with it. You may say things like “enroll Sue” rather than “make an enrollment for Sue”, but when talking about a long-running process it makes sense to say “how is Sue’s enrollment coming along?”. That’s why using a noun for any process that lasts long enough for us to want to know how it’s going looks better.

Reification of abstract concept

Dictionary meaning of Reification is “make (something abstract) more concrete or real”. In other words, reification makes something abstract (e.g. a concept) more concrete/real.

With coarse grained approach focusing on business capabilities, we are modeling many more reified abstract notions as resources. A good example of reified resource is CustomerEnrollment that we discussed previously. Instead of using the Customer resource, we are using a resource which is the equivalent of a request to enroll customer. Let us consider two other examples from the same banking domain:

  1. Cash deposit in bank account: Customer deposits money to his/her account. This involves the operations such as applying business rules (example: checking if the deposited amount is within the allowed limit), updating customer’s account balance, adding a transaction entry, sending notifications to customer’s mobile or email, etc. Though we could technically use the Account resource here, a better option would be to reify the business capability / abstract concept called transaction (or money deposit) and create a new resource “Transaction”.
  2. Money transfer between two bank accounts: Customer transfers money from one bank account to another bank account. This involves updating two low level resources (“from” account and “to” account), also involves business validations, creation of transaction entry, sending notifications, etc. If the “to” account is in another bank, the transfer might be channeled via a central bank or an external agency. Here the abstract concept is “money transfer” transaction. To transfer money, we can post to /transactions or /accounts/343/transactions and create a new “Transaction” (or “MoneyTransfer”) resource. It is important to note that creation of new “Transaction” resource does not automatically imply creation of a database table for “Transaction”. API design should be independent of the underlying design concerns on API implementation and data persistence.

In both these cases, rather than using the Account resource, we are using a resource which is the equivalent to a command to deposit money or transfer money - Transaction resource (similar to CustomerEnrollment mentioned previously). This is a good idea especially if this could be a long running process (for example: money transfer might involve multiple stages before it completes). This of course doesn't preclude you from having an Account resource as well - one could be updated as a result of the “Transaction” being processed. Also, there may be genuine use cases for making API requests to “Account” resource. For example, to get the account summary/balance information, the API request should made to “Account” resource.

One of the key switches in thinking is to understand that there is an infinite URI space that you can take advantage of. At the same time, it is good to avoid resource proliferation that may add confusion to the API design. As long as there is a genuine need for the resources with clear user/consumer “intent” that fits well in the overall API design, URI space can be expanded. Reified resources can be used as the transactional boundary for your service.

There’s another aspect to this - the way you organize the server behavior is separate to how the API works. We’ve already discussed having a “Transaction” resource for money deposit, and there are many good reasons for doing so. But it’s also perfectly valid for money deposit to be handled by a post to the Account resource. The service that handles the Account is then responsible for coordinating the changes and create a Transaction resource, Notification resource, etc. (which may be in the same service, or separate services). There’s no reason for the client to have to do all this itself. API provider needs to pick one service to handle the coordination responsibility. In our example, this responsibility is given to the service handling the Transaction resource, if that’s the route to take. This is just the same as in-memory object design. When a client needs to coordinate changes over a bunch of objects a common approach is to pick one to handle the coordination.

REST without PUT and CQRS

HTTP verb PUT can be used for idempotent resource updates (or resource creations in some cases) by the API consumer. However, use of PUT for complex state transitions can lead to synchronous cruddy CRUD. It also usually throws away a lot of information that was available at the time the update was triggered - what was the real business domain event that triggered this update? With “REST without PUT” technique, the idea is that consumers are forced to post new 'nounified' request resources. As discussed earlier, changing a customer’s mailing address is a POST to a new “ChangeOfAddress” resource, not a PUT of a “Customer” resource with a different mailing address field value. The last bit means that we can reduce our API consumers’ expectations of atomic consistency - if we POST a “ChangeOfAddress”, then GET the referenced Customer, it's clearer that the update may not have been processed yet and the old state may be still there (asynchronous API). GETing the “ChangeOfAddress” resource that was created with “201” response will return details related to the event, and a link to the resources that were updated or that will be updated.

The idea is that we no longer PUT the "new" state of an entity, instead we make our mutations be first class citizen nouns (rather than verbs), and POST them. This also plays very nicely with event sourcing - events are a canonical example of first class citizen nouns and help us get out of the mindset of thinking of them as "mutators" - they're domain relevant events, not just a change to the state of some object.

REST without PUT has a side-benefit of separating command and query interfaces (CQRS) and forces consumers to allow for eventual consistency. We POST command entities to one endpoint (the "C" of CQRS) and GET a model entity from another endpoint (the "Q"). To expand this further, think of an API that manages Customers. When we want to view the current state of a Customer, we GET the Customer. When we want to change a Customer, we actually POST a “CustomerChange” resource. When we view the customer via the GET, this may just be a projection of the current state of the Customer built up from the series of change events related to the Customer. Or it could be that we have the “CustomerChange” resources that actually mutate the state of Customer in DB, in which case the GET is a direct DB retrieval. In the latter case, we may be destroying some data (associated with the change events) and potentially losing the intent behind the change (depending whether we choose to persist the event data or not). So REST without PUT doesn't mean we *will* get CQRS automatically, but does make it very easy if we want to.

In summary, PUT puts too much internal domain knowledge into the client as discussed earlier. The client shouldn't be manipulating internal representation; it should be a source of user intent. On the other hand, PUT is easier to implement for many simple situations and has good support in libraries. So, the decision needs to be balanced between simplicity of using PUT versus the relevance of event data for the business.

An example from the public GitHub API

GitHub API is a good example of a reasonably well designed API in the public domain with right resource granularity. For example, creating a fork is an asynchronous operation. GitHub supports the reified “Forks” sub-collection resource that can be used to list existing forks or create a new fork. Performing code “merge” using merges sub-collection resource is another example of reification of the “merge” concept and the underlying merge operation. On the other hand, GitHub also supports many low level resources such as Tag. Most of the real world APIs will require both coarse grained aggregate resources and also low level “nouns in the domain” resources.

On a closing note

As with everything else, there is no single approach that will work for all the situations. As discussed earlier, there may be situations where an API that is built around low level resources may just be fine. For example, to get bank account balance information, an API that is built around “Account” resource is good enough. On the other hand, if the need is to do a money transfer or to get a bank statement, the API needs to be built around the coarse grained “Transactions” resource. Similarly, there are many situations where using HTTP PUT on low level domain resources may be appropriate and simple. There are also situations where the state transitions are complex and long running or event data is business relevant and worth capturing using HTTP POST on user/consumer “intent” resources. Resource modeling requires a careful consideration based on the business needs, technical considerations (clean design, maintainability, etc.) and cost-benefit analysis of various approaches discussed earlier so that the API design brings out the best API consumer interaction experience.

Acknowledgement
This article significantly borrows from the discussion points of ThoughtWorks employees Charles Haynes, Duncan Cragg, Evan Bottcher, Graham Brooks, James Lewis, Martin Fowler, Peter Gillard-Moss, Samir Seth, Sam Newman, Sarah Hutchins, Srinivasan Raguraman and Tarek Abdelmaguid in internal ThoughtWorks developer group discussions, which the article’s author was part of. With additional input from: Jonny Leroy, Sriram Narayan and Tarek Abdelmaguid.