Entendendo REST API
Overview
Mergulhar no mundo das REST APIs pode parecer um desafio, mas não tem que ser! Este post é um convite para descomplicar e entender de forma clara e objetiva os conceitos por trás da arquitetura REST, os métodos HTTP, as boas práticas de API e muito mais. Se você estava procurando por um guia introdutório cheio de insights e sem complicações, acompanhado de exemplos práticos, veio ao lugar certo. Prepare-se para simplificar sua jornada no desenvolvimento de APIs e otimizar a comunicação entre suas aplicações!
Estes dias precisei fazer umas pesquisas sobre REST API e acabei com um artigo introdutório sobre o assunto, então resolvi compartilhar o produto final. A ideia aqui não é mergulhar nos detalhes técnicos, mas passar pelos pontos chave e conceitos essenciais.
O que é REST API?
REST ou Representational State Transfer é uma arquitetura de software criada no ano 2000 por Roy Fielding (em sua dissertação de doutorado). Esta arquitetura padroniza formas para criação de webservices, permitindo criação de serviços e integrações que utilizam HTTP como forma de comunicação.
Qual a diferença entre REST API e um Webservice “normal”?
Ambos funcionam como uma forma de comunicação. A grande diferente é que um Webservice facilita a interação entre duas maquinas, enquanto que uma API atua como uma interface entre duas aplicações diferentes, para que elas possam realizar integrações e se comunicar.
Quando um Webservice é concebido, ele (geralmente) utiliza uma interface que pode ser compreendida por outra maquina através da implementação de padrões como o WSDL (Webservice Description Language). Já a API define a forma com que um programa se comunica com outro. Ele pode implementar também SOAP, REST e XML-RPC (chamadas remotas utilizando XML) como uma forma de comunicação.
Geralmente estas comunicações (entre API"s) são feitas via HTTP, mas isso não é um requisito. O Office utuliza VBA e APIs baseadas em objetos COM para se comunicar, que não utiliza o protocolo HTTP. Outras formas de comunicação utilizadas são objetos COM, DLLs (arquivos .H no C/C++) e arquivos .JAR ou RMI (Java).
Resumindo, um Webservice é uma forma de se disponibilizar a API rest, utilizando HTTP como forma de comunicação.
Qual a diferença entre REST e RESTful?
REST faz referencia a arquitetura Representational State Transfer enquanto que o termo RESTful é utilizado para indicar um webservice que aplica os conceitos da arquitetura.
Arquitetura REST
Uma das propostas da arquitetura REST é uniformizar a interface utilizada na implementação da API. A analise da necessária para a aplicação dest arquitetura deve considerar as seis regras do padrão e seus impactos nos elementos do sistema. Deve-se, primeiro, conhecer o que será exposto na API em termos conceituais e de negócio. Não comece pensando nas regras e na aplicação da API. Primeiro responda: O que existe na aplicação? Como (e por que) estes elementos se interagem? Depois que responder estas questões, você deve começar a aplicar as regras. Esta “técnica” é conhecida como principio estilo nulo.
(Observação: As regras mencionei acima são, em inglês, referidas como “constraints” mas, em português, esta palavra geralmente é entendida como restrição e/ou limitação. Sendo assim, achei mais adequado traduzi-la como regra.)
De todo modo, existem 6 destas regras/constraints.
1) Client-Server: A primeira regra para implementação do REST é a definição da separação entre as responsabilidades do cliente e do servidor. Ao separar o que é referente a interface do usuário e o que é referente a busca e armazenamento de informações, aumentamos a portabilidade da UI para múltiplas plataformas e melhoramos a escalabilidade da aplicação, pois os componentes do servidor foram simplificados. Este conceito é especialmente importante para aplicações Web, pois esta separação permite que os componentes evoluam de forma independente;
2) Stateless: Indica que toda comunicação com o servidor deve ser feita de forma independente, ou seja, toda comunicação deve conter todas as informações necessárias para concluir uma determinada operação. Nenhuma comunicação (request) entre o cliente e o servidor pode utilizar qualquer informação armazenada no lado do servidor. O estado da sessão deve ser mantido apenas no cliente.
Existem 3 grandes vantagens na aplicação desta regra. A primeira é que ela melhora a visibilidade das requisições, pois o servidor (ou sistema de monitoramento) precisa analisar apenas a requisição que foi recebida para determinar a natureza dela. A segunda é a resiliência, pois é mais simples para o servidor recuperar de falhas pontuais, se todas as informações que ele precisa estão em apenas uma request. A terceira é a escalabilidade, pois não é necessário manter recursos em memória e/ou compartilhar componentes complexos, uma vez que cada request é independente.
Nada no mundo é perfeito. Existem também desvantagens para esta implementação. Uma desvantagem que deve ser mantida em mente é que, como as requisições são sempre enviadas de forma completa, a quantidade de informações trafegada aumenta, pois não existe um contexto onde as informações serão compartilhadas entre cliente e servidor. Outra desvantagem é que, uma vez que as informações da sessão são mantidas no lado do cliente, é responsabilidade do servidor garantir que elas estão sempre consistentes.
3) Cache: Para otimizar o trafego de informações, são adicionadas sinalizações para a criação do Client-Cache-Stateless (CSS, mas não confundir com os estilos utilizados em conjunto com html). Essa sinalização indica que os dados contidos na resposta da request podem (cacheable) ou não (non-cacheable) ser armazenados localmente para reuso.
A vantagem desta implementação é que melhora a eficiência, pois elimina interações desnecessárias), melhora escalabilidade e por eliminar a latência entre algumas requisições, melhora a performance aparente para o usuário.
A desvantagem é que o usuário pode estar utilizando uma informação que já não é mais valida e/ou completa.
4) Interface Uniforme: Possivelmente, esta é a regra principal para a arquitetura REST: A uniformidade das interfaces entre componentes. A ideia desta regra é deixar visível, de forma geral, a arquitetura do sistema e suas interações. Estas interações ficam desacopladas do resto do serviço, o que permite que cada componente evolua de forma independente. Para criação de uma interface unica eficiente (ou mais eficiente possível) é necessário que quatro conceitos sejam aplicados: identificação de recursos, manipulação destes recursos através de representações, mensagens auto-explicativas e e hypermedia como motor do estado da aplicação (HATEOAS).
A grande desvantagem desta regra é que ela não é exatamente eficiente, pois as informações transferidas vem em um formato padronizado e não na forma da necessidade específica da aplicação. A interface REST é muito eficiente para transferência de dados granulares, que é otimizada para o uso comum na Web, mas isso pode resultar em implementações não tão eficientes para outras formas de interação.
5) Sistema em camadas: Para melhorar a compatibilidade e escalabilidade da aplicação, são aplicadas camadas no sistema. Elas permitem que componentes sejam organizados de forma hierarquizada, o que restringe o comportamento dos níveis subordinados, pois eles não podem (ou não devem) conseguir ver/conhecer elementos que estão acima da sua hierarquia. Ao restringir um componente à sua hierarquia, a complexidade do sistema é limitada e promove a independência dos elementos, que ficam desacoplados. Estas camadas também podem ser utilizadas para encapsular serviços legados e proteger novos serviços de clientes legado. Outra ação que pode ser tomada é mover funcionalidades pouco utilizadas para uma camada intermediária.
A combinação desta regra com a anterior (Interface Uniforme) gera uma arquitetura similar ao estilo “pipe and filter”, já que o fluxo de informações pela rede vai sendo filtrado na medida em que os filtros são aplicados (ou na medida em que você vai “descendo” na hierarquia dos elementos). Note que aqui não estou falando das rotas (caminhos expostos pela API), eles estão na regra anterior. O ponto desta regra são as camadas de serviço que compõe a API.
Em outras palavras, o cliente não vai conseguir saber se está conectado diretamente ao servidor ou se está em um servidor intermediário. Estes intermediários podem ser utilizados para melhorar escalabilidade e permitir balanceamento de carga ao prover caches compartilhados, além de permitirem aplicação de politicas de segurança.
A grande desvantagem destas camadas é que elas adicionam latência e um overhead ao processar a informação, mas estes pontos podem ser balanceados com a criação de caches nas fronteiras entre as camadas.
6) Código sob demanda: Esta última regra é opcional e pode parecer estranha ou irrelevante, mas pode prover funcionalidades essenciais, dependendo da sua aplicação. Ela permite que a expansão das funcionalidades do cliente através do download e execução de extensões no formato de applets ou scripts. Isso simplifica a execução do cliente, uma vez que ele não precisa ter todas as funcionalidades implementadas previamente. Esta é uma regra complexa de ser implementada, pois você precisa conhecer todos os seus clientes. Por exemplo: Para utilizar um applet java sob demanda, você precisa ter certeza de que todos os seus clientes suportam Java e mesmo assim, pode “trombar” em politicas de segurança e firewalls que bloqueiem o download e execução.
Terminologia REST:
- Recurso (resource): conceitualmente falando, o alvo da interação desejada.
- Identificador de recurso (Resource identifier): URL ou URN do recurso. Algo que seja unico e o identifique.
- Coleções (collections): Agrupamentos de recursos. (Exemplo: Cachorro é um item da Coleção de animais).
- Representação (representation): Como o recurso é representado – Documento HTML, JSON, imagem (png, jpg, etc).
- Metadata da Representação (representation metadata): tipo de midia, data da última modificação (last-modified-time)
- Metadata do Recurso (resource metadata): link da fonte, alternativas e variações para aquele recurso, caso exista.
- Informações de Controle (control data): cache-control, if-modified-since
Endpoints da API
Os endpoints são os caminhos que serão utilizados para acessar determinados recursos.
Em APIs que não implementam rest, encontramos endpoints neste formato:
(considerando o recurso: Carro)
- /listaCarros
- /adicionaCarro
- /atualizaCarro
- /removeCarro
- /removeTodosCarros
Ou em algumas APIs menos desorganizados:
- /carro/novo
- /carro/atualiza
- /carro/remove
- /carro/removeTodos
O problema com este tipo de formato de API é que existirão milhares de endpoints diferentes para assuntos relacionados e com assuntos redundantes neles.
Então, qual a forma correta para criar estes endpoints?
A melhor forma (considerando os padrões REST) é criar o endpoint /carros. Assim mesmo, sem ações explicitas nele. O que vai definir quais ações serão executadas são os verbos utilizados na requisição. Estes verbos são os métodos HTTP (get, post, delete, put).
Sendo assim:
- /listaCarros vira uma requisição GET para o /carros;
- /adicionaCarro vira uma requisição POST para o /carros (passando os dados do carro que será adicionado)
- /atualizaCarro vira uma requisição PUT para /carros/{id_do_carro}
- /removeCarro vira uma requisição DELETE para /carros/{id_do_carro}
Como podemos verificar, a utilização da API ficou bem mais consistente e simples. Ao invés de precisarmos ter em mãos centenas de rotas diferentes, temos uma rota lógica e padronizada, que aceita diversos tipos de requisição.
Métodos HTTP
- GET: Feito para buscar informações. Não altera nada e as informações recuperadas não devem gerar efeitos colaterais;
- POST: Criar novo elemento para uma determinada entidade;
- PUT: Utilizado para atualizar uma entidade;
- PATCH: Utilizado para atualização parcial da entidade;
- DELETE: Remove entidade
Acho que vale diferenciar um pouco mais o PUT do PATCH. No PUT, você vai enviar o objeto inteiro e o sistema vai atualizar os valores dele. No PATCH, você vai enviar apenas o que está sendo atualizado.
Exemplo:
Objeto (do tipo Carro) inserido via POST /carros: { “marca”: “Tesla”, “modelo”: “S”, “qtd_portas”: null }
Este objeto foi inserido com ID 42 ea agora você precisa atualizar a quantidade de portas.
Uma request PUT /carros/42 ficaria assim: { “marca”: “Tesla”, “modelo”: “S”, “qtd_portas”: 4 }
Enquanto que uma request PATCH /carros/42 ficaria assim: { “qtd_portas”: 4 }
A ideia do PUT é atualizar a entidade inteira, ou seja, se você enviar apenas a quantidade de portas em um request PUT, em teoria, este objeto se torna a representação da entidade inteira. Isso não acontece com o PATCH.
Tenha em mente que alguns proxys aceitam apenas GET e POST. Quando for assim, você pode precisar utilizar o header X-HTTP-Method-Override para informar o método que você realmente quer utilizar.
Códigos de respostas HTTP
Todas as requests HTTP que forem respondidas, devem ter um código. Existem diversos códigos, categorizados por “família”.
- 2xx: Sucesso;
- 3xx: Redirecionamento;
- 4xx: Erro no lado do cliente;
- 5xx Erro no lado do servidor;
Na categoria 2xx (sucesso), temos os retornos mais comuns:
- 200: Ok. Tudo deu certo. Utilizado com GET, PUT or POST;
- 201: Created. Conteúdo enviado foi criado com sucesso. O retorno de um POST, após criar uma nova instancia de uma entidade, deve ser sempre este.
- 204: No Content. Indica que a requisição foi bem sucedida, mas nenhum dado foi retornado. Uma request com o método DELETE é um ótimo candidato para utilizar este código de retorno;
Na categoria 3xx (redirecionamento):
- 304 Not Modified. Indica que a resposta desta requisição já está no cache, não sendo necessário fazer outro request.
Na categoria 4xx (Erro no lado do cliente):
- 400 Bad Request. Indica que a requisição feita não foi processado, pois o servidor não conseguiu “entender” do que se tratava, ou seja, ela não foi elaborada corretamente;
- 401 Unauthorized. Indica que a requisição desejada exige autenticação e esta não foi fornecida;
- 404 Not Found. Possivelmente o único código de retorno que todo mundo (mesmo os que não sabem programar) conhecem! Indica que o recurso requisitado não foi encontrado;
- 410 Gone. Indica que o recurso desejado não está mais disponível, pois foi deliberadamente movido.
Na categoria 5xx (Erro no lado do servidor):
- 500 Internal Server Error. Indica que ocorreu o famoso “erro inesperado” no servidor, ou seja, algum exception e ele não sabe o que fazer;
- 503 Service Unavailable. Indica que o servidor e/ou serviço estão fora do ar no momento. Pode indicar manutenção.
Falando em respostas…
O que é HATEOAS?
Esta é a sigla formada pelo nome Hypermedia As The Engine of Application State e serve para prover informações adicionais sobre o recurso que está sendo trabalhado. Isso é importante, pois se considerarmos que as interações com o REST são atômicas e o conhecimento é compartimentalizado, pode ser que o cliente não tenha qualquer conhecimento sobre como interagir com o recurso desejado.
Um exemplo utilizando a entidade Carro:
GET /carros/42
{
"id": 42,
"marca": "Tesla",
"modelo": "S",
"qtd_portas": 4,
"links": [{
"rel": "self",
"href": "/carros/42"
},
{
"rel": "acessorios",
"href": "/carros/42/acessorios"
}]
}
No exemplo acima, temos dois links que vieram, em adição objeto requisitado. O primeiro foi o com a referência self, que indica a URL para acessar este recurso novamente. O segundo foi uma referência para lista os recursos do tipo acessórios para este carro.
É uma forma de expor ao usuário como ele pode interagir com esta entidade. Através do HATEOAS você pode disponibilizar o acesso scripts e applets, quando (e se) implementar a regra código sob demanda.
Retornos parciais, Pesquisa, filtros, ordenação e paginação
Todas estas ações são adições as queries que já são feitas na aplicação. Por razões que me parecem obvias (e simples), elas serão utilizadas apenas para requests do tipo GET.
Outro ponto de atenção é que nenhuma destas ações é obrigatória, mas é legal implementar tantas quanto possível. Vamos ser legais com nossos usuários! 🙂
Explicando um pouco cada ação dessas:
Ordenação
Quando a ordenação for pertinente, o comum é utilizar a definição dela via querystring, onde você vai indicar que deve ser realizada uma ordenação e qual propriedade deve ser utilizada.
Exemplos:
- /carros?ordenacao=nome | ordena de forma ascendente, por nome do carro;
- /carros?ordenacao=nome_desc | ordena de forma descendente (explicita), por nome do carro;
- /carros?ordenacao=nome&?ord_modo=desc | ordena de forma descendente (explicita), por nome do carro;
Filtro
Esta ação é especialmente útil quando temos um volume grande de dados para tratar.
Exemplos:
- /carros?marca=Tesla | Busca carros da marca Tesla;
- /carros?marca=Tesla&modelo=S | Busca carros da marca Tesla e do modelo S;
- /carros?filtro=(marca:Tesla,modelo:S) | Busca carros da marca Tesla e do modelo S;
Pesquisa
Busca entidades com determinada características.
Exemplos:
- /carros?busca=Tesla | Busca carros da marca Tesla;
- /carros/busca?marca=Tesla&modelo=S | Busca carros da marca Tesla e do modelo S;
- /carros?busca=(marca:Tesla,modelo:S) | Busca carros da marca Tesla e do modelo S;
Paginação
Outra opção quando temos um grande dataset é utilizar paginação. Existem diversas formas de implementar esta ação, uma mais complexas que outras. Uma das mais simples que consigo pensar agora seria:
Exemplos:
- /carros?pagina=42 | Retorna a pagina 42 dos registros de carros;
- /carros?pagina=42&limite=100 | Retorna a pagina 42 dos registros de carros, sendo que cada pagina está limitada a 100 registros;
Retornos parciais
Esta ação permite que você retorne apenas o que a aplicação realmente precisa. Este tipo de abordagem se faz necessário quando a quantidade de dados trafegada é crucial. Pode ser complicada a implementação, mas seus usuários vão ficar muito mais felizes com você, quando estiverem utilizando seu sistema via 3G.
Exemplos:
- /carros?atributos=marca | Retorna uma lista com as marcas de cada carro
- /carros?atributos=marca, modelo | Retorna uma lista com objetos contendo apenas marca e modelo
- ?fields=url,object(content,attachments/url) | Esta é a forma que a Google utiliza
- &fields=likes,checkins,products | Esta é a forma que o Facebook utiliza
- /people/~:(id,first-name,last-name,industry) | Esta é a forma que o LinkedIn utiliza
Um ponto de atenção para utilização de querystring é que se você adicionar muitos parâmetros, pode acabar recebendo um erro 414 URI Too Long do servidor. O tamanho máximo da url varia de acordo com servidores e navegadores. Todavia, se você estiver com uma URL longa o suficiente para apresentar este erro, talvez precise repensar o que está fazendo com a aplicação.
Retornando entidades relacionadas
Continuando a utilização da entidade Carro, vamos supor que ela possui uma lista de acessórios e outra lista com as rodas do carro. Quando você acessa a rota /carros, você recebe uma coleção com todos os carros disponíveis, mas não recebe a referencia para as duas outras entidades (acessórios e rodas)… mas e se você precisar da entidade completa?
Bom, isso pode ser feito através da utilização de uma ação conhecida como include (ou embed ou expand ou incluir, se você quiser em português). Todavia, mantenha em mente que isso é um pequeno hack, pois não adere as regras do REST, mas aplicações complexas podem precisar deste tipo de informação sem precisar realizar diversas requisições.
Exemplo de chamada:
GET /carros/42?include=acessorios,rodas
Com a chamada acima você recebe o objeto da entidade Carros referente ao id 42, com as respectivas listas de acessórios e rodas.
Versionamento
Após a publicação da sua API, você possuirá clientes utilizando-a, ou seja, se você alterar o funcionamento de algum endpoint, pode “quebrar” a aplicação do cliente. Lembre-se: API é a forma de comunicação entre duas aplicações. Sendo assim, quando você for evoluir sua API, utilize o versionamento na rota.
Exemplos:
- /api/v1/carros | Primeira versão da API
- /api/v2/carros | Segunda versão. A primeira ainda existe, mas esta pode ser apenas uma evolução com funcionalidades extras ou uma API completamente diferente.
Níveis de maturidade do REST
Um dos artigos que encontrei foi o do Martin Fowler, onde ele fala sobre os níveis de maturidade das APIs .
Neste artigo ele fala sobre 4 níveis. Abaixo está o resumo deles:
- Nível 0 – A API da vergonha: Neste cenário, você envia uma requisição para /carroService e este endpoint te retorna as URIs que você pode utilizar e você terá diversas rotas para o mesmo recurso (/adicionarCarro, /editarCarro, /atualizarCarro, etc) e é, na verdade, um aglomerado de endpoints bagunçados;
- Nível 1 – Recursos: Neste ponto, a API já tem os recursos separados. Aqui você já não tem mais a rota /editarCarro, você tem a /carros/{idCarro}. As coisas estão ficando mais organizadas, mas ainda falta um pouco;
- Nível 2 – Verbos HTTP: Quando a API chega neste nível, você não utiliza mais apenas POST e GET. Utiliza também os outros verbos, da forma como eles foram concebidos. Sendo assim, nada mais de modificar entidades a partir de requests GET, ok?
- Nível 3 – Controles Hypermedia: O último nível na evolução das APIs. Aqui sua API utiliza HATEOAS para fornecer as ações disponíveis para cada recurso.
Melhores práticas
- Utilize pronomes para listas recursos. Nunca utilize verbos. (estes ficam restritos aos métodos http e as ações enviadas via querystring);
- Padronize o nome dos seus recursos no plural e no mesmo idioma! (esta ultima parte é importante para nós, brasileiros);
- Requisições do tipo GET nunca devem realizar alterações!
- Utilize sub-recursos para entidades relacionadas. Exemplo: /carros/42/acessorios/1
- Utilize os headers da request HTTP pra serialização de formatos. No HTTP-Header, inclua o Content-Type para definir o tipo da request e o Accept para definir o tipo de resposta que será aceita;
- Utilize o HATEOAS;
- Forneça uma forma para o cliente ordenar, paginar e filtrar coleções;
- Versione sua API (/api/v1/carros, /api/v2/carros, etc…)
- Não devolva apenas o código de erro, envie também um payload com uma explicação do que aconteceu;
- Sempre permita o override dos métodos HTTP (com X-HTTP-Method-Override)
Referencias
- https://en.wikipedia.org/wiki/Representational_state_transfer
- https://medium.com/@programmerasi/difference-between-api-and-web-service-73c873573c9d
- https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
- https://www.restapitutorial.com/lessons/whatisrest.html
- https://stackoverflow.com/questions/1568834/whats-the-difference-between-rest-restful
- https://stackoverflow.com/questions/28459418/rest-api-put-vs-patch-with-real-life-examples
- https://en.wikipedia.org/wiki/HATEOAS
- https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design
- https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/
- https://martinfowler.com/articles/richardsonMaturityModel.html