Este artigo apresenta o estilo REST de construção de web services, bem como as melhores práticas comumente adotadas na criação de uma RESTful API. Seguindo as práticas analisadas aqui, o leitor poderá melhorar a performance, a manutenibilidade e a usabilidade das APIs projetadas, tornando-as completas e intuitivas para os desenvolvedores que as utilizarão.
Construindo uma RESTful API – Parte 1
Este tema é útil para desenvolvedores que precisem criar uma API pública ou privada para expor dados e funcionalidades de um sistema web para outras aplicações, sejam elas web, mobile ou desktop.
À medida que um sistema web vai evoluindo, é comum que se torne necessário uma nova forma de se comunicar com ele. Comumente, serão humanos que o usarão em um browser para acessar e modificar seus dados. Porém, pode chegar o momento em que desejamos tornar possível que outras aplicações se comuniquem com nosso sistema, seja com o intuito de disponibilizar outros clientes (muitas vezes nativos a alguma plataforma, como o Android) ou para expor informações de acordo com os requisitos de outros desenvolvedores. Quando chega esse momento, precisamos de web services para prover acesso a esses dados.
Para entender melhor, exploraremos o exemplo do Facebook. Devido ao seu grande crescimento como rede social, ele encontrou diversas oportunidades que o ajudaram a crescer ainda mais. Entre elas, podemos destacar duas que envolvem a criação de web services: a expansão do mercado mobile, exigindo aplicações nativas para tais dispositivos, e a expansão de suas funcionalidades através de aplicações de terceiros.
Essas oportunidades têm em comum a necessidade de web services para suprir informações para as aplicações externas. Afinal, a web comum é baseada em HTML, que possibilita uma boa interação do sistema com pessoas. Entretanto, quando a aplicação precisa interagir com outras aplicações, essas páginas não são uma boa opção.
Assim como o Facebook, há momentos em que necessitamos desenvolver web services para nossas aplicações. Nesses momentos, duas siglas vêm à nossa mente: SOAP e REST. Muitos vão conhecer melhor SOAP, que significa Simple Object Access Protocol e tenderão a escolhê-lo sobre o REST, que significa Representational State Transfer. Discutir qual é o melhor está fora do escopo deste artigo. Porém, considerando os motivos do Facebook, que também podem ser os seus, justificaremos o porquê da escolha do REST.
Para iniciar essa discussão, vamos primeiro rever os motivos do Facebook. Em resumo, ele desejava prover acesso para aplicações clientes, principalmente mobile, e a outros sistemas de terceiros. Nesse contexto, analisaremos a seguir o que torna o REST a melhor opção:
- O SOAP é uma especificação de um protocolo estruturado em XML. Devido ao tamanho de suas mensagens, elas naturalmente ocupam mais rede, o que pode ser um problema para aplicações mobile;
- A complexidade na comunicação com um web service SOAP também é um problema para aplicações mobile e HTML5, visto que são necessárias bibliotecas específicas, utilização de geradores de código e ainda a manutenção do código gerado nas diferentes plataformas;
- O SOAP foi construído com o pensamento da comunicação entre servidores, em que é assumido que eles são naturalmente seguros. Não se pode garantir que uma aplicação rodando em um dispositivo de um usuário possua tal segurança;
- O REST é mais simples. Foi projetado para ser usado em clientes “magros”, o que o torna ideal para utilização em dispositivos com capacidades limitadas;
- As respostas do REST são cacheáveis, diferente do SOAP. Isso dá um grande aumento de performance em clientes simples;
- Enquanto o SOAP utiliza apenas XML, o REST pode se comunicar através de diversos formatos, sendo JSON o mais usado. Por ser um formato menos verboso e normalmente menor, a transferência de dados com o uso de JSON causa uma carga menor na rede, além de ser mais facilmente consumido por clientes construídos com qualquer linguagem, em especial HTML5.
Como podemos verificar, no contexto apresentado, a melhor opção é o REST. Mas isso não significa que ele é perfeito e fácil de implementar. Consumir um web service bem construído e bem documentado que utiliza REST é fácil e dá grande produtividade para o desenvolvedor.
Porém, o mesmo não se aplica para a sua construção. Veremos que são muitos os detalhes a serem considerados na construção de uma boa RESTful API para outros desenvolvedores.
REST
Diferente do SOAP, que é uma especificação, o REST é algo mais abstrato: um estilo arquitetural. Ele é formado por um conjunto de regras no topo do protocolo HTTP que foram comunicadas pela primeira vez por Roy Fielding em sua tese de doutorado. Depois de seu surgimento, começou a se tornar uma alternativa mais leve de comunicação entre computadores (cliente e servidor).
Para entendê-lo, devemos saber cada uma de suas seis regras. Para ser considerado REST, o web service deve ser ou possuir: Interface uniforme, Stateless, Cacheável, Cliente-Servidor, Sistema em camadas e Código sob demanda. Veremos em detalhes cada uma delas a seguir.
Interface uniforme
Essa regra define que deve haver uma interface uniforme de comunicação entre o cliente e o servidor. O objetivo dela é torná-los independentes um do outro. Isso facilita a criação de uma API, ou seja, um contrato a ser seguido por terceiros na utilização da mesma. Dessa forma, assim como acontece com interfaces no Java, os utilizadores não precisam saber os detalhes da implementação.
Em cima dessa regra, são definidos alguns princípios. O primeiro deles é que o REST deverá ser baseado em recursos identificados por URLs. Diferente de SOAP, que é baseado em ações, como “obter produto”, o REST é baseado no recurso, deixando a definição das ações para os métodos HTTP. Portanto, quando quisermos obter um recurso, como um produto, faremos uma requisição GET para a URL que o localiza.
Assim, é importante entendermos que as URLs serão definidas por substantivos e não por verbos, salvo quando for necessário fazer algum cálculo ou algo que não esteja vinculado a um recurso no servidor. O motivo disso é bem simples: tornar a interface simples. A Listagem 1 mostra exemplos de URLs para um recurso Produto que não seguem esse padrão e a Listagem 2 mostra exemplos que o seguem.
/listarProdutos = URL para listar todos os produtos
/obterProduto/1 = URL para obter o produto 1
/criarProduto = URL para criar produto
/deletarProduto = URL para deletar produto
/produtos = URL para ações sobre recursos do tipo Produto
/produtos/1 = URL para ações sobre um produto específico
Em ambas as listagens foram mostradas apenas o CRUD básico desse recurso. Porém, a Listagem 2 parece incompleta. Afinal, onde definimos as ações? Elas são definidas utilizando os métodos do protocolo HTTP: GET, POST, PUT, DELETE e HEAD. Veremos mais a frente como cada um desses métodos é usado, mas pode-se adiantar que os quatro primeiros serão traduzidos nas operações de CRUD.
Ainda observando os exemplos da Listagem 2, podemos ter outra dúvida: por que utilizar o plural? Bem, essa resposta é bem direta: simplicidade novamente. Utilizando o plural, as operações de leitura para uma lista ficam naturais, mas parece menos natural quando nos referimos a uma única instância do recurso. No entanto, por simplicidade, mantemos todas as URLs de determinado recurso com a mesma raiz (parte inicial da URL), que por padrão está no plural.
Outra definição importante é que não obteremos recursos através dessas chamadas ao servidor, mas sim suas representações. Por isso a sigla REST significa Representational State Transfer. Transferimos do servidor para o cliente e vice-versa representações de um recurso, sendo eles representados de diversas formas. Usa-se predominantemente JSON, mas também é comum representá-los no formato XML.
Essa ideia de representações de recursos também pode ser identificada na Web. Nela, o usuário localiza um recurso por uma URL. Esse recurso é mostrado de forma legível para humanos através de uma representação em HTML, a qual é interpretada pelo navegador e o mesmo cria uma visualização para o usuário. Aqui estamos fazendo algo parecido. A diferença é que usamos representações a serem lidas e interpretadas por outras aplicações. Afinal, diferente de seres humanos, elas não precisam da aparência visual, e sim dos dados.
Podemos levar essa comparação adiante introduzindo mais um princípio definido pelo REST: HATEOAS. Essa sigla significa Hypermedia as the Engine of Application State. Esse princípio define que o estado da aplicação, ou seja, o estado dos recursos desta, é transferido entre o cliente e o servidor na forma de hypermedia: hypertexto com hyperlinks.
Isso significa que, dada a requisição por um recurso, tanto o seu link quanto os links dos recursos relacionados serão retornados no corpo da resposta para essa requisição. Assim, aplicações clientes podem ser guiadas a outros recursos através dos hyperlinks fornecidos pelo próprio resultado de uma chamada à API.
Se compararmos isso com a Web que conhecemos, podemos perceber que o HTML que o servidor retorna para nosso navegador também é uma hypermedia. Nela estão links que usamos para encontrar outros recursos relacionados, o que faz com que possamos ser guiados para outros recursos facilmente sem precisarmos necessariamente saber a URL que os localiza.
Para exemplificar isso no contexto de REST, imagine que estamos utilizando a API do Facebook para obter as postagens de um usuário. Como é esperado que tivessem diversos itens como resultado, é natural pensarmos numa paginação dos mesmos. Então, quando fazemos a requisição, podemos ver que o próprio resultado já nos retorna um hyperlink para a próxima página de resultados. Esse é apenas um exemplo real usado para mostrar como as representações dos recursos são interligadas na forma de hypermedia. Isso é feito através da utilização de URLs para identificar os recursos.
O último princípio a ser apresentado dentro dessa regra de Interface Uniforme é o que diz respeito às mensagens serem autodescritivas. Isso significa que as mensagens devem trazer informações sobre o formato de representação a ser utilizado, a possibilidade de fazer cache dos dados, entre outras informações que as tornam completas.
Veremos mais sobre os formatos mais adiante. No momento, continuaremos expondo as regras definidas pelo REST.
Stateless
Essa é uma das regras mais importantes do REST. Ela dita que cada requisição deverá conter todas as informações relevantes para ser processada. Ou seja, não deve haver manutenção de estado entre as requisições. Caso haja alguma manutenção de estado, esse deverá ser mantido pelo cliente e enviado a cada nova requisição.
Assim, a única responsabilidade do servidor é utilizar os dados recebidos na requisição, fazer o processamento necessário e retornar tudo que for pertinente ao cliente.
Isso pode parecer estranho a princípio para quem está acostumado a desenvolver para a Web, em que sempre possuímos um estado guardado no servidor para cada cliente. Porém, essa regra abre caminho para obter uma enorme escalabilidade, visto que o servidor não precisará manter uma sessão e se comunicar utilizando-a.
Com isso, poderíamos criar clusters de servidores espalhados e ter um balanceador de carga sem nos preocuparmos se a requisição do usuário será tratada por um servidor que possui seus dados de sessão.
Veremos mais a frente que essa regra é descumprida em alguns casos, como o caso da three-legged OAuth, que é uma especificação de segurança. Mas essa regra deve ser sempre seguida, a não ser em casos especiais.
Cacheável
O servidor, sempre que possível, deverá indicar ao cliente a possibilidade do mesmo fazer cache dos resultados retornados. Essa capacidade de fazer cache reduz o tráfego de dados entre o cliente e o servidor, melhorando a escalabilidade do servidor e melhorando a performance de ambos. Veremos mais a frente como podemos utilizar uma técnica baseada em ETag para se obter tal capacidade.
Cliente-Servidor
Apoiado na regra da interface uniforme, esta é a regra que separa as responsabilidades do cliente e do servidor. Ela indica que é de responsabilidade do servidor o armazenamento dos dados, o que aumenta a portabilidade do cliente. Também diz que é de responsabilidade exclusiva do cliente a apresentação da interface com o usuário, bem como a manutenção da sessão dele, o que torna os servidores mais escaláveis.
Assim, o cliente e o servidor poderão ser desenvolvidos de maneira independente, enquanto a interface de comunicação entre eles permanecer inalterada.
Sistema em camadas
Essa regra abre possibilidade para a divisão do servidor em um sistema em camadas, visto que ela afirma que a interface da API deve funcionar de tal forma que o cliente não consiga diferenciar se está sendo atendido pelo servidor final, um servidor que utiliza os serviços do final ou mesmo se o servidor atual é o mesmo que atendeu as requisições anteriores.
Assim, a escalabilidade é mais uma vez aumentada, visto que podemos configurar um cluster de servidores conectados por um balanceador de carga, sem que o cliente precise notar isso.
Código sob demanda
A regra que diz que o servidor poderá estender as funcionalidades do cliente através da transferência de lógica a ser executada pelo mesmo, como um script em JavaScript a ser executado pelo cliente, é a única regra opcional. Ela apenas indica a possibilidade do servidor fazer isso, mas não torna tal funcionalidade necessária para definir um serviço como REST.
Com isso, concluímos as seis regras que compõem o estilo arquitetural do REST. Nosso próximo objetivo é entender melhor como as ações sobre os recursos são vinculadas aos métodos HTTP.
HTTP Methods
Como foi discutido anteriormente, os recursos são identificados por URLs com a mesma raiz, que é construída com o nome do recurso no plural. A partir disso, todas as ações sobre esses recursos são feitas utilizando métodos HTTP: GET, POST, PUT, DELETE e HEAD. Isso mostra que, diferente da Web comum que conhecemos, o REST faz um uso mais avançado das capacidades do protocolo HTTP, utilizando os outros métodos não suportados nela, visto que a mesma possui apenas requisições dos tipos GET e POST.
Antes de mapearmos cada um desses métodos para suas respectivas ações sobre um recurso, vamos falar sobre um atributo que alguns deles têm em comum: a idempotência.
Idempotência
Uma ação idempotente é aquela que produzirá o mesmo resultado independente de quantas vezes ela seja executada. Isso significa que quando executarmos uma ação idempotente múltiplas vezes em um recurso, seu estado ficará de tal forma como se tivéssemos executado apenas uma vez.
Esse atributo é importante, pois há momentos em que não temos certeza se nossa requisição chegou ao servidor. Isso pode acontecer, por exemplo, se a internet cair durante a requisição. Não saberemos se o servidor executou nossa requisição e não conseguiu responder devido ao problema de comunicação ou se nossa requisição nem mesmo chegou ao servidor.
Se a ação que estamos tentando executar for idempotente, podemos simplesmente repetir a requisição, visto que esse tipo de ação garante a manutenção do estado do recurso como se uma única requisição tivesse sido enviada.
Esse atributo é muito importante, mas não está presente em todas as ações possíveis sobre um recurso. À medida que explorarmos cada um dos métodos HTTP com suas respectivas ações, indicaremos se o mesmo possui ou não idempotência.
HTTP GET
O método GET é um dos que deixa mais claro, pelo seu próprio nome, qual tipo de operação ele realiza. Naturalmente, esse tipo de requisição é mapeada para a operação de leitura do recurso. O resultado para ela é que uma representação, em determinado formato, do recurso, é retornada pelo servidor no corpo da resposta.
Temos algumas opções nesse tipo de requisição. Podemos requisitar uma única instância de um recurso, utilizando seu ID na URL como visto anteriormente, e também requisitar uma coleção de recursos. Em ambos os casos, podemos dizer ao servidor em qual formato queremos a representação desse recurso, sendo em JSON ou XML, por exemplo, dependendo da disponibilidade desses formatos no servidor.
Também veremos mais a frente que uma boa prática a ser implementada é a possibilidade de paginação da coleção de recursos. Além disso, veremos em breve como selecionar os campos desejados, fazer um filtro na recuperação, entre outras boas práticas.
Ainda sobre a requisição do tipo GET, por ser uma operação de leitura, é naturalmente idempotente, visto que ela nem mesmo altera o estado do recurso. Portanto, caso haja uma falha de conexão, podemos simplesmente repetir a requisição sem se preocupar com qualquer efeito colateral.
Mais detalhes sobre as respostas para essas requisições, incluindo o status HTTP retornado pelo servidor em diferentes situações, serão vistos adiante.
HTTP DELETE
Assim como o GET, o nome do DELETE também é bem intuitivo e deixa bem claro o que ele faz: deleta uma ou mais instâncias de um recurso. Essa requisição pode ser executada para uma instância única utilizando o ID na URL ou para todas as instâncias através da raiz do recurso. Também é possível deletar múltiplos recursos específicos a partir de uma filtragem dos resultados, o que será visto mais a frente.
Porém, por ser uma operação crítica, é comum que o servidor a disponibilize apenas para requisições com o ID a ser deletado.
Sendo essa uma operação que remove totalmente um recurso, ela é idempotente. A única diferença que pode acontecer em múltiplas chamadas é que as chamadas consecutivas serão respondidas pelo servidor com uma mensagem que indique que o recurso não foi encontrado. Naturalmente, com essa resposta, o cliente pode inferir que o recurso foi deletado em uma das requisições anteriores.
HTTP HEAD
O método HEAD é um dos menos comuns e normalmente não é disponibilizado pelo servidor. Seu objetivo é parecido com o GET, com a diferença que ele retornará apenas metadados sobre o recurso requisitado, ao invés de trazer suas representações completas. Essa requisição poderia ser usada para saber informações sobre uma coleção de recursos, como a quantidade total, e também para se obter informações sobre um recurso específico, como a data da última modificação.
Deste modo, como o GET, essa requisição também é idempotente, visto que não causa nenhuma alteração de estado nos recursos.
HTTP POST
O método POST é usado na Web comum para se criar e atualizar um determinado recurso através de um formulário. No contexto de REST, ele é usado de forma similar. POST possui duas funções: criar um recurso (sem especificar o ID do mesmo, de forma a deixar que o servidor o gere) e fazer uma atualização parcial do mesmo.
Por atualização parcial, entendemos como uma atualização em que apenas alguns campos de uma instância de recurso são modificados, deixando os outros sem qualquer modificação.
Quando utilizamos esse método para criar um recurso, deixando que o servidor gerencie seu ID, naturalmente precisamos saber com qual ID ele foi criado. Portanto, o servidor deve responder a essa requisição com a localização do recurso, sendo essa a URL que unicamente o localiza.
Através dessa URL, sabemos a localização e o ID do mesmo, que são informações importantes para continuarmos interagindo com ele. Pode ser, também, que o servidor adicione na resposta a representação desse novo recurso. Isso pode muitas vezes ser inútil, já que o cliente possui a representação que acabou de ser criada no servidor, mas pode fazer sentido em alguns momentos em que tal recurso possua outros campos gerados pelo servidor.
Por sua natureza de criação de um recurso novo ou atualização parcial de um recurso existente, esse método não é idempotente. Se repetirmos a requisição de criação, teremos múltiplos recursos criados com estado duplicado. No caso da requisição de atualização parcial, podemos deixar o recurso inconsistente se outra requisição desse tipo for realizada por outro cliente em paralelo. Portanto, esse método deve ser utilizado com cuidado, devido à sua natureza não idempotente.
HTTP PUT
O método PUT não é utilizado na Web comum, mas podemos imaginar seu significado pelo seu nome. Através desse nome, imaginamos que ele “coloca” algo no servidor. Assim, ele possui duas funções: fazer uma atualização completa de determinado recurso (especificado pela URL) e criar um recurso especificando o ID.
Como é comum que seja de responsabilidade do servidor a criação do ID de seus recursos, sua segunda função é menos comum e pode não ser disponibilizada pelo servidor.
Quando falamos sobre uma atualização completa, estamos especificando que os valores de todos os campos do recurso serão alterados com essa requisição. Portanto, é comum que ela aconteça depois de uma requisição do tipo GET que obtenha todos os campos atuais e então o usuário tem a chance de modificá-los antes que eles sejam enviados ao servidor.
Se o servidor disponibilizar a operação de criação de um recurso com seu ID, as requisições de atualização e criação ficam idênticas, fazendo o papel de: atualize se existir ou crie caso contrário. Nesse caso, não é necessário que o servidor responda com a localização do recurso, visto que o ID foi especificado pelo cliente. Mas pode ser que ele responda com o estado atual do recurso, se for pertinente.
Diferente do POST, esse método sempre modifica completamente o estado de um recurso localizado em determinada URL. Isso impede que haja inconsistências no estado e que múltiplos recursos sejam criados por acidente. Portanto, essa operação é idempotente.
Como podemos ver, o REST utiliza muito bem os métodos HTTP, separando as responsabilidades das ações sobre os recursos para cada um desses métodos. Mas, além disso, ele também faz grande uso dos headers do HTTP, do status da resposta, dos parâmetros passados na query string, entre outros. Tudo isso será visto a seguir.
HTTP Status
Quando falamos dos status do HTTP, nos lembramos dos status clássicos usados na Web: 200 OK, 404 Not Found, 500 Internal Server Error, entre outros. No geral, a não ser que haja um erro no servidor, o usuário tente uma URL inexistente ou algum outro caso específico, o servidor sempre responde com 200 OK, mostrando possíveis erros para o usuário dentro da própria página HTML. Isso acontece porque estamos nos comunicando com humanos e a melhor forma de mostrar resultados de uma requisição para ele é com uma página bem apresentada.
Isso não é verdade quando queremos nos comunicar com outros computadores. Nesse caso, fazemos uso intenso dos status disponíveis no HTTP para facilitar o tratamento do lado cliente. Mais a frente, veremos que é uma boa prática também incluir algumas informações extras na resposta quando houver uma situação inesperada, para que o desenvolvedor do software cliente do web service entenda a situação.
Porém, devemos sempre utilizar os status disponíveis pelo protocolo HTTP em nossas respostas.
Para dar uma ideia melhor sobre o uso dessa importante capacidade, mostraremos a seguir alguns dos principais status HTTP junto com uma explicação sobre onde poderíamos usá-los.
200 OK
Esse é o status mais comum e que é usado na maioria das respostas de requisições bem sucedidas. Podemos retorná-lo quando as seguintes requisições procedem com sucesso:
- GET de uma instância ou coleção de recursos. Esse status indica que a representação desejada se encontra no corpo da resposta;
- DELETE sobre um ou mais recursos. Essa resposta significa que os recursos foram encontrados e deletados do servidor;
- PUT na criação com ID especificado ou na atualização de determinado recurso;
- POST na atualização completa de determinado recurso.
201 Created
Quando um recurso é criado com sucesso, retornamos esse status e adicionamos um header na resposta chamado Location. Esse header conterá uma URL para o novo recurso. Esse status é usado basicamente na criação de um recurso sem a especificação do ID através de POST, apesar de ter sentido o retornarmos também quando criarmos com o ID especificado através do PUT.
304 Not Modified
Esse status é de grande importância na utilização de cache por parte do cliente. Pode-se usar um código que represente o estado de um recurso e o cliente pode enviar tal código obtido de uma requisição anterior. Assim, o servidor pode verificar se houve alguma mudança no recurso e, caso contrário, retornar apenas esse status, sem nenhum corpo na resposta, para indicar que o cliente pode fazer uso da resposta obtida anteriormente e armazenada no cache. Com isso, pode-se evitar o tráfego extra na rede.
Veremos mais adiante como criar essa capacidade no servidor para auxiliar os clientes a fazerem cache das requisições de GET.
400 Bad Request
Esse status, como o próprio nome indica, deve ser utilizado quando o cliente enviar uma requisição mal formada, seja ela por falta de parâmetros, má formação dos parâmetros, entre outras possibilidades.
401 Unauthorized
Apesar de seu nome indicar que o cliente não está autorizado a executar tal requisição, o nome utilizado não está de acordo com o real sentido desse status. O objetivo dele é, na verdade, indicar que o cliente não está autenticado, ao invés de autorizado. Ou seja, isso indica que o cliente deverá prover uma forma de autenticação e tentar novamente.
Para aqueles que não estão acostumados com os termos utilizados na área de segurança, o cliente está autenticado quando ele prova sua identidade ao servidor, enquanto o fato de estar autorizado indica que, sabendo sua identidade, o servidor reconhece que ele pode acessar determinado recurso.
A autenticação é realizada antes, enquanto a autorização é realizada como forma de verificação se determinado usuário, já identificado, possui direitos o suficiente. Portanto, não se confunda com o nome desse status.
403 Forbidden
Esse é o verdadeiro status que indica que o cliente, já identificado, não possui direito de acesso suficiente para realizar determinada ação sobre determinado recurso. Em outras palavras, o cliente não está autorizado a realizar essa requisição e não deve tentar novamente, a não ser que venha a obter autorização para tal.
Unauthorized e Forbidden são usados quando preparamos a segurança da API. Veremos adiante as boas práticas na implementação dessa segurança.
404 Not Found
Esse status é usado para indicar que um recurso específico identificado por determinada URL não foi encontrado. Tal status é retornado em requisições GET, DELETE e também na atualização completa através do POST, quando a instância desejada não é encontrada no servidor.
Com o entendimento dos métodos utilizados no REST e dos status mais retornados como resposta pelo servidor, temos o suficiente para construir uma API com o estilo arquitetural do REST. Porém, se começássemos agora, provavelmente criaríamos uma API pobre e de difícil utilização, pois ainda não foram apresentadas as boas práticas. Portanto, o próximo passo é apresentar as boas práticas que foram emergindo na criação de APIs nesse estilo.
Boas práticas na criação de uma REST API
Foi visto até agora as regras do estilo arquitetural REST e como ele utiliza as capacidades do protocolo HTTP. Oficialmente, isso é o REST. Porém, naturalmente foram emergindo boas práticas que passaram a ser adotadas no desenvolvimento de RESTful APIs. Ao seguirmos essas práticas, garantimos que nossa API vai possuir boas funcionalidades, ser de fácil uso para outros desenvolvedores e de fácil manutenção.
Tais práticas definem o design da API, o que influenciará também na implementação da mesma. Daqui em diante, cada uma dessas práticas será exposta.
Versionamento
Quando projetamos nossa API, definimos uma interface de comunicação a ser utilizada por outras aplicações. Podemos comparar essa interface com as clássicas interfaces do Java, que definem os métodos, mas sem explicitar a implementação dos mesmos. Os clientes de nossa API a verão da mesma forma. Eles lerão a documentação que especifica o funcionamento do web service.
Nessa documentação serão especificados quais recursos estão disponíveis, quais as URLs dos mesmos, os parâmetros esperados, etc. Através dessas especificações se tem um contrato de como a API vai funcionar, mesmo sem saber como ela é implementada.
Enquanto não houver mudanças nessa interface, pode-se dar manutenção normalmente no código que a implementa. Porém, pode ser que precisemos mudar essa interface por algum motivo. Quando isso acontecer, só podemos ter uma certeza: todas as implementações de clientes que confiem no contrato estabelecido por ela deixarão de funcionar. Então, como podemos lidar com isso?
Para resolver esse problema fazemos um versionamento da interface da API. Assim, obrigamos o cliente a especificar qual versão ele está invocando. Assim, quando houver uma modificação na interface, avisamos os clientes e damos um prazo para eles migrarem para essa nova versão, deixando a versão antiga funcionando por um prazo determinado. Vale notar que estamos falando aqui de uma mudança de interface e não de implementação. As mudanças de implementação deverão ser transparentes aos usuários, desde que a interface definida seja respeitada.
Mas o que exatamente é a interface, o contrato com o cliente? Esse contrato é composto pela sintaxe das requisições e respostas sobre os recursos. Nessa sintaxe estão inclusos: a URL do recurso, os headers da requisição HTTP, a estrutura da resposta e representação do recurso, opções disponíveis que alteram de alguma forma o resultado da requisição, entre outros.
Qualquer alteração que mude a forma de interação com determinado recurso deve ser versionada, pois tal alteração poderá fazer com que as implementações criadas por determinado cliente sejam quebradas, vindo a falhar.
Porém, nem toda alteração da interface precisa necessariamente sofrer versionamento. Se adicionarmos um campo na representação de um recurso, por exemplo, não afetaremos os clientes existentes, que simplesmente ignorarão tal campo. Portanto, devemos sempre pensar no quanto uma modificação afeta nossos clientes.
Qualquer alteração na interface que possa fazer com que a implementação do cliente venha a falhar, deverá ser versionada.
Entendido o porquê e quando devemos versionar, podemos nos perguntar como especificar as versões. Pode-se pensar em criar versões com alto grau de granularidade, como 1.0.1, 2.1.3, etc. Mas essa não é a forma mais recomendada. Ao invés disso, especificamos números simples, deixando nossas versões na forma v1, v2, etc.
O motivo disso é que estamos falando de uma interface e interfaces não devem mudar com frequência. Números com alto grau de granularidade dão a impressão de que haverá modificações constantes nas versões e esse não é o caso.
Com a numeração de versões decidida, falta especificar como os desenvolvedores poderão comunicar ao servidor qual versão será empregada para tratar sua requisição. Nesse ponto, há muita discussão entre aqueles que acreditam que a versão a ser utilizada da API deve estar na URL e aqueles que acreditam que ela deve ser especificada como tipo de mídia da requisição, seja no header Accept do HTTP em requisições GET ou no header Content-Type em requisições POST e PUT.
Antes de argumentar quais das duas deveram utilizar, podemos ver exemplos de versionamento através da URL na Listagem 3 e através de tipo de mídia na Listagem 4.
- GET api.devmedia.com.br/v1/revistas
Accept: application/json
- GET api.devmedia.com.br/v2/revistas/190
Accept: application/json
- POST api.devmedia.com.br/v1/revistas
Content-Type: application/json
Body:
{
nome: “Java Magazine”,
isbn: “1676-8361”
}
- GET api.devmedia.com.br/revistas
Accept: application/vnd.devmedia.revistas-v1+json
- GET api.devmedia.com.br/revistas/190
Accept: application/vnd.devmedia.revista-v2+json
- POST api.devmedia.com.br/revistas
Content-Type: application/vnd.devmedia.revista-v1+json
Body:
{
nome: “Java Magazine”,
isbn: “1676-8361”
}
Podemos ver que nos exemplos da Listagem 3, inserimos um v1 e um v2 no início da URL para indicar qual versão da API estamos utilizando. Enquanto na Listagem 4, modificamos o header que indica qual formato de conteúdo estamos esperando nas requisições GET e o header que indica qual o tipo de conteúdo estamos enviando nas requisições POST e PUT. Como foi dito, há bastante discussão sobre qual das duas abordagens é mais recomendada.
Um dos argumentos para a utilização da URL é a clareza e a facilidade de se ver qual versão da API está sendo usada. Essa abordagem também facilita os testes da API em clientes HTTP mais simples, como navegadores. Além disso, ela é adotada nas APIs de grandes empresas como Google, Twitter, Dropbox, entre outros. Portanto, é natural que os desenvolvedores estejam acostumados com essa forma de versionamento.
Porém, existem muitas críticas para a adoção da URL no versionamento. Uma delas diz que ela fere o princípio de HATEOAS do REST, visto que links de recursos obtidos e armazenados anteriormente poderão não funcionar mais quando a versão que eles utilizavam deixar de ser mantida. Também dizem que as mudanças de contrato significam primariamente mudanças nas representações dos recursos e não exatamente nos recursos.
Portanto, o lugar certo para se definir a versão é no tipo de mídia usado nessa representação.
Além desses, existem diversos argumentos e muita discussão acerca da utilização de uma abordagem ou de outra. Cada uma delas possui suas vantagens e desvantagens e cabe ao desenvolvedor escolher qual será a adotada. Para ajudar na decisão, pode-se ler sobre o assunto em alguns dos links contidos no final do artigo ou buscar mais opiniões em fóruns e artigos.
Seja lá qual for a opção escolhida, sempre se deve manter em mente a importância do versionamento da API.
Formato da representação
Para representarmos um recurso, podemos utilizar diversos formatos. Porém, dois deles se sobressaem e são mais conhecidos e usados: XML e JSON. O primeiro é usado na representação de recursos e transporte de dados há bastante tempo, tanto por web services SOAP, como REST, além de outras aplicações. Já o JSON é um formato um pouco mais recente e que vem tomando o espaço do XML devido a diversos fatores.
O poder do XML de representar dados é indiscutível, tendo inclusive um poder maior do que o JSON e outros formatos. Porém, se compararmos o tamanho de um recurso representado através de XML com o mesmo representado através de JSON, veremos uma grande diferença. Por ser mais simples, o JSON ocupa um espaço muito menor e isso interfere bastante na quantidade de banda necessária na transferência dos dados.
Isso acontece porque, junto com os dados, o XML também carrega muito texto descartável que é apenas usado para manter sua estrutura. Enquanto isso, o JSON carrega pouquíssimo texto além dos dados necessários.
Outro ponto forte do JSON é sua integração com JavaScript, o que facilita o desenvolvimento de aplicações com HTML5. Afinal, JSON significa JavaScript Object Notation e é basicamente uma representação textual de um objeto no JavaScript. Por isso, sua representação é muito mais compatível com a representação de objetos da maioria das linguagens, diferente do XML.
Para melhor visualizarmos a diferença de simplicidade e tamanho entre os dois, podemos visualizar as Listagens 5 e 6. Cada uma delas mostra o mesmo recurso representado através de XML e JSON, respectivamente.
<?xml version="1.0"?>
<cliente>
<nome>Fernando Camargo</nome>
<cpf>62854828532</cpf>
<idade>22<idade>
<telefones>
<telefone>6298350912</telefone>
<telefone>6232903845</telefone>
</telefones>
<email>fernando.camargo.ti@gmail.com</email>
<endereco>
<logradouro>Rua 12-A</logradouro>
<cep>63905192</cep>
<bairro>Setor Aeroporto</bairro>
<numero>168</numero>
<complemento>Apartamento 301</complemento>
<cidade>Goiânia</cidade>
<estado>GO</estado>
<pais>Brasil</pais>
</endereco>
</cliente>
{
"nome": "Fernando Camargo",
"cpf": "62854828532",
"idade": 22,
"telefones": ["6298350912", "6232903845"],
"email": "fernando.camargo.ti@gmail.com",
"endereco": {
"logradouro": "Rua 12-A",
"cep": "63905192",
"bairro": "Setor Aeroporto",
"numero": 168,
"complemento": "Apartamento 301",
"cidade": "Goiânia",
"estado": "GO",
"pais": "Brasil"
}
}
Observando as duas listagens, notamos que é mais fácil de ler e de escrever a representação no formato JSON. Também observamos que o XML possui muitas repetições devido à abertura e fechamento de cada tag. Além disso, uma lista de valores, como é o caso do telefone neste exemplo, acaba desperdiçando ainda mais texto, precisando da repetição da tag <telefone>.
Ainda podemos notar que os objetos são expressos de forma muito mais natural através de JSON. Isso tudo é devido à simplicidade desse formato e pelo fato de ter sido criado a partir da notação de objetos do JavaScript.
Para verificarmos a diferença de tamanho, podemos remover todos os espaços desnecessários do texto e conferir o tamanho final de cada um dos arquivos. Ao fazermos isso com o exemplo do cliente, obtemos 466 bytes com o XML e 314 bytes com o JSON.
A diferença pode parecer pequena devido ao tamanho do objeto, mas se transferirmos uma representação com uma lista de clientes, veremos uma diferença muito maior. Para exemplificar, podemos criar uma lista de dez clientes utilizando esse mesmo cliente em cada uma das representações. Nesse simples caso, já atingimos 4,4KB com XML e 3,1KB com JSON.
Se considerarmos que nosso cliente é uma aplicação móvel com uma internet limitada e um hardware não tão potente, o uso de JSON causa uma grande diferença na quantidade de dados transferida e no tempo de interpretação dos dados. Quando lidamos com esse tipo de aplicação, qualquer redução de gasto na banda da internet é bem vinda.
Apesar de toda essa vantagem do JSON, ainda é comum que várias APIs deem a opção aos desenvolvedores para escolher qual representação ele quer usar. Muito disso vem da tradição do uso de XML e do costume de utilizá-lo. Porém, por outro lado, o Twitter e outros grandes donos de APIs deixaram de dar suporte ao XML e passaram a suportar somente o JSON como formato de representação. Ao projetarmos uma API, devemos decidir se vamos adotar apenas um desses dois ou se vamos suportar os dois. A princípio, é recomendada a utilização de JSON pelos motivos já discutidos.
Ainda nesse tópico, é comum que alguns desenvolvedores representem os campos no JSON utilizando a notação de nome de variáveis comum em sua linguagem. Alguns usam nomes separados com underline, como primeiro_nome, e outros usam o camelCase, como primeiroNome. Para manter a conformidade com o JavaScript, é recomendado o uso do camelCase, independente da linguagem de preferência do desenvolvedor.
Um último assunto importante no tópico de formato é quanto à formatação de datas e horários. Em uma API REST usada por todo o mundo, podem acontecer diversos problemas devido ao uso de diferentes time zones pelo servidor e seus clientes. Para evitar tais erros, é recomendado utilizar o padrão ISO 8601, que define que a time zone seja sempre UTC e que sempre siga o formato yyyy-MM-dd'T'HH:mm:ss.SSS'Z', sendo esse texto uma String que pode ser usada na API Java com a classe SimpleDateFormat. Para exemplificar, observe essa data e hora: 2013-09-26T21:24:39.521Z.
Seguindo esses padrões, clientes ao redor de todo o mundo poderão utilizar sua API de forma fácil e evitando erros comuns.
Identificação e ligação entre recursos
O gerenciamento da identificação dos recursos e a ligação entre eles é um dos detalhes mais importantes em uma API REST. Nesse momento, devemos nos preocupar ao máximo com o conceito de HATEOAS. Isso significa que os recursos serão localizados por hyperlinks e proverão os hyperlinks para recursos relacionados.
Portanto, o primeiro passo é incluir um novo campo em todo e qualquer recurso: href. Esse campo será disponibilizado no lugar do campo de identificação. Ou seja, ao invés de termos um campo ID na representação, teremos o campo “href”. Isso porque ele terá como valor uma URL que unicamente localiza tal recurso. Assim, o cliente da API poderá gravar esse hyperlink para identificar o recurso e poderá requisitá-lo novamente mais tarde.
A Listagem 7 mostra o exemplo do Cliente apresentado anteriormente com esse novo campo. Deve-se prestar atenção no fato de não haver um campo de ID, mas esse pode ser extraído da URL, se desejado pelo usuário da API.
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"idade": 22,
"telefones": ["6298350912", "6232903845"],
"email": "fernando.camargo.ti@gmail.com",
"endereco": {
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”,
"logradouro": "Rua 12-A",
"cep": "63905192",
"bairro": "Setor Aeroporto",
"numero": 168,
"complemento": "Apartamento 301",
"cidade": "Goiânia",
"estado": "GO",
"pais": "Brasil"
}
}
Esse campo não é importante apenas para a identificação do recurso, mas também para fazer a ligação dos recursos relacionados. Pode-se reparar na Listagem 7 que o endereço retornado também possui um href, o que nos permite fazer outros tipos de requisição em cima do mesmo, caso queiramos modificá-lo utilizando um PUT ou um POST, por exemplo. Veremos no próximo tópico que podemos, em alguns momentos, retornar apenas o href do recurso relacionado, ao invés de retorná-lo por completo.
Representação parcial e paginação
Este tópico é muito importante para melhorar a performance dos clientes e do servidor. Através de representações parciais e paginação, podemos reduzir a quantidade de dados trafegados pela rede, tornando menor a necessidade de banda para suprir os clientes e também fazendo com que a requisição seja respondida com um atraso menor.
Começamos falando sobre representações parciais. Elas são feitas dando uma forma ao cliente para especificar quais informações ele quer sobre determinado recurso. Ou seja, ele seleciona quais campos deverão ser retornados na representação, ao invés de obter sempre o recurso completo.
Isso é importante porque nem sempre o cliente necessitará do recurso completo para seus propósitos. Assim, deve-se prover uma forma do mesmo identificar quais campos deverão ser retornados pelo servidor em requisições de GET.
Para essa e outras opções, é comum utilizarmos a query-string da requisição GET para se passar os parâmetros necessários. Assim, teremos no final da URL requisitada os parâmetros na forma já conhecida de query-string: <URL>?parametro1=valor1¶metro2=valor2. Utilizando essa abordagem, podemos receber um parâmetro chamado “fields”, onde será recebida a lista de campos desejados do recurso.
Para exemplificar o uso do parâmetro fields, consideremos o recurso Cliente apresentado anteriormente e que um dos usuários da API deseja obter apenas o nome, CPF e e-mail do cliente. A Listagem 8 mostra a requisição feita e a resposta em JSON.
GET https://api.javamagazine.com.br/clientes/2g41s?fields=nome,cpf,email
Resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"email": "fernando.camargo.ti@gmail.com"
}
Como podemos ver nessa listagem, o cliente consegue especificar os campos desejados passando os nomes dos mesmos separados por vírgula, formando a seguinte sintaxe: ?fields=<fields,>. Porém, não vimos ainda como especificar os campos de recursos relacionados, como é o caso do endereço no nosso exemplo.
Nesse caso, adicionamos o campo do recurso relacionado acompanhado de parênteses, dentro dos quais listamos os campos internos desejados. Podemos ver um exemplo disso na Listagem 9, que mostra a requisição para os mesmos campos da Listagem 8, com a adição da cidade, o estado e o país do endereço do cliente.
GET https://api.javamagazine.com.br/clientes/2g41s?fields=nome,cpf,email,endereco(cidade,estado,pais)
Resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"email": "fernando.camargo.ti@gmail.com",
"endereco": {
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”,
"cidade": "Goiânia",
"estado": "GO",
"pais": "Brasil"
}
}
Se não especificarmos os campos desejados de um recurso relacionado, devemos seguir uma abordagem relacionada ao tópico anterior, sobre a ligação de recursos. Na verdade, essa abordagem deve ser seguida mesmo quando o cliente não especificar os campos desejados do recurso requisitado, ou seja, quando pedir a representação completa. Ela consiste em retornar, por padrão, o recurso relacionado incompleto, contendo apenas seu href, como mostrado nas Listagens 10 e 11.
GET https://api.javamagazine.com.br/clientes/2g41s?fields=nome,cpf,email,endereco
Resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"email": "fernando.camargo.ti@gmail.com",
"endereco": {
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”
}
}
GET https://api.javamagazine.com.br/clientes/2g41s
Resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"idade": 22,
"telefones": ["6298350912", "6232903845"],
"email": "fernando.camargo.ti@gmail.com",
"endereco": {
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”
}
}
Com isso, estamos seguindo o princípio do HATEOAS e retornando uma resposta mais curta por padrão. Porém, deve-se dar a opção ao usuário de expandir a representação do recurso relacionado, caso ele deseje obtê-lo na mesma requisição.
Para adicionar essa opção, utilizamos o parâmetro expand na query-string. Ele funcionará da mesma forma que o parâmetro fields, sendo que a representação final terá os campos listados expandidos. Sua utilização é mostrada na Listagem 12.
GET https://api.javamagazine.com.br/clientes/2g41s?expand=endereco
Resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"idade": 22,
"telefones": ["6298350912", "6232903845"],
"email": "fernando.camargo.ti@gmail.com",
"endereco": {
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”,
"logradouro": "Rua 12-A",
"cep": "63905192",
"bairro": "Setor Aeroporto",
"numero": 168,
"complemento": "Apartamento 301",
"cidade": "Goiânia",
"estado": "GO",
"pais": "Brasil"
}
}
A utilização desse parâmetro de expansão da representação não deve ser feita apenas com recursos que se relacionem de um para um. Poderíamos ter, por exemplo, uma coleção de endereços. Essa coleção deveria vir apenas com os hyperlinks por padrão, dando a possibilidade de expansão através desse parâmetro. A Listagem 13 exemplifica a resposta padrão para esse caso e a Listagem 14 exemplifica a resposta com os endereços expandidos.
GET https://api.javamagazine.com.br/clientes/2g41s
Resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"idade": 22,
"telefones": ["6298350912", "6232903845"],
"email": "fernando.camargo.ti@gmail.com",
"enderecos": [{
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”
},{
"href": “https://api.javamagazine.com.br/enderecos/7hf852”
}]
}
GET https://api.javamagazine.com.br/clientes/2g41s?expand=enderecos
Resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"idade": 22,
"telefones": ["6298350912", "6232903845"],
"email": "fernando.camargo.ti@gmail.com",
"enderecos": [{
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”,
"logradouro": "Rua 12-A",
"cep": "63905192",
"bairro": "Setor Aeroporto",
"numero": 168,
"complemento": "Apartamento 301",
"cidade": "Goiânia",
"estado": "GO",
"pais": "Brasil"
},{
"href": “https://api.javamagazine.com.br/enderecos/7hf852”,
"logradouro": "Rua 17-A",
"cep": "740705420",
"bairro": "Setor Central",
"numero": 68,
"complemento": "",
"cidade": "Goiânia",
"estado": "GO",
"pais": "Brasil"
}]
}
Além disso, podemos ter esse tipo de resposta com apenas hyperlinks como padrão também para requisições por determinada coleção de recursos. Esse é o caso de um GET na URL raiz de um recurso, que já foi visto anteriormente retornando os recursos expandidos. Poderíamos, por exemplo, requisitar diretamente a lista de endereços e o servidor responderia com a lista de hyperlinks ou os endereços completos caso o cliente requisite a expansão.
Como podemos ver, a combinação dos parâmetros “fields” e “expand” traz grande flexibilidade para o cliente, enquanto permitem uma otimização no tamanho dos dados retornados, de forma que o usuário da API obterá apenas o necessário para sua aplicação. Porém, ainda falta um tópico importante nesse sentido. Ele diz respeito à paginação das respostas.
Imagine uma requisição de listagem dos produtos de um sistema. Se esse sistema tiver milhares de produtos, a resposta será gigantesca e possivelmente desnecessária. Portanto, precisamos de uma forma de paginar por esses resultados. Para resolver isso, poderíamos requisitar os dez primeiros e, quando necessário, requisitar os próximos dez e assim por diante.
Então, a API deve estar preparada para receber parâmetros que definem quais itens o cliente deseja. Tal opção pode ser disponibilizada de duas formas: através de parâmetros que indiquem o index inicial e o final, ou através de parâmetros que indiquem o index inicial e a quantidade de itens.
Na maioria dos casos, para tornar mais fácil para o cliente, a segunda abordagem é escolhida. Assim, um parâmetro, que indicaria a quantidade de itens, é fixada, e o cliente varia apenas o parâmetro do index inicial para paginar. Os nomes comumente adotados são: offset e limit, sendo que offset define o “deslocamento” do item inicial, ou seja, qual seria o index do primeiro item a ser retornado, enquanto limit define a quantidade limite a ser retornada.
Com o exemplo em que obteríamos os itens de dez em dez, poderíamos fazer a primeira requisição com o offset setado para 0, ou seja, a partir do primeiro item, e limitar a quantidade de itens retornados a 10, através do parâmetro limit. A próxima requisição manteria o valor do limit e aumentaria para 10 o valor do offset. Assim, à medida que fosse necessário, faríamos novas requisições somando o valor do offset com o valor utilizado no limite, obtendo todos os itens aos poucos, caso necessário.
Entretanto, qual deve ser o comportamento da API quando seu usuário não passar esses dois parâmetros? Ela deverá possuir valores padrões para cada um deles e adotá-los quando algum desses parâmetros não for passado pelo cliente. Normalmente, adota-se o valor padrão do offset como 0 e o valor do limit varia entre 10 e 25, dependendo da decisão do desenvolvedor da API.
Porém, ainda temos mais um requisito: seguir o que é definido pelo HATEOAS. Ou seja, devemos garantir que nossa API retorne hyperlinks para a próxima página, página anterior, primeira e última página, além de alguns metadados, como o offset e o limit utilizado, caso o usuário não tenha passado esses parâmetros, e outros metadados úteis. Na Listagem 15 vemos uma requisição sem os parâmetros de paginação acompanhada de seu resultado e na Listagem 16 vemos a requisição seguinte que utilizará o hyperlink recebido no campo next da resposta anterior.
GET https://api.javamagazine.com.br/clientes
Resposta:
{
"href": "https://api.javamagazine.com.br/clientes",
"offset": 0,
"limit": 10,
"first": {"href": "https://api.javamagazine.com.br/clientes?offset=0"},
"previous": null,
"next": {"href": "https://api.javamagazine.com.br/clientes?offset=10"},
"last": {"href": "https://api.javamagazine.com.br/clientes?offset=990"},
"items": [
{"href": "https://api.javamagazine.com.br/clientes/61h58s"},
{"href": "https://api.javamagazine.com.br/clientes/f72baf"},
{"href": "https://api.javamagazine.com.br/clientes/823huf"},
{"href": "https://api.javamagazine.com.br/clientes/u23hf6"},
{"href": "https://api.javamagazine.com.br/clientes/fqgy2f"},
{"href": "https://api.javamagazine.com.br/clientes/vuhq9df"},
{"href": "https://api.javamagazine.com.br/clientes/vg782qw"},
{"href": "https://api.javamagazine.com.br/clientes/v90aqga"},
{"href": "https://api.javamagazine.com.br/clientes/vihaqgt9"},
{"href": "https://api.javamagazine.com.br/clientes/v7qafld"}
]
}
ET https://api.javamagazine.com.br/clientes?offset=10
Resposta:
{
"href": "https://api.javamagazine.com.br/clientes",
"offset": 10,
"limit": 10,
"first": {"href": "https://api.javamagazine.com.br/clientes?offset=0"},
"previous": null,
"next": {"href": "https://api.javamagazine.com.br/clientes?offset=20"},
"last": {"href": "https://api.javamagazine.com.br/clientes?offset=990"},
"items": [
{"href": "https://api.javamagazine.com.br/clientes/72r286"},
{"href": "https://api.javamagazine.com.br/clientes/7ncau2"},
{"href": "https://api.javamagazine.com.br/clientes/v3q9fu"},
{"href": "https://api.javamagazine.com.br/clientes/03nfi8a"},
{"href": "https://api.javamagazine.com.br/clientes/vin3w9"},
{"href": "https://api.javamagazine.com.br/clientes/v90wkas"},
{"href": "https://api.javamagazine.com.br/clientes/g30qgl9"},
{"href": "https://api.javamagazine.com.br/clientes/fuhqknag"},
{"href": "https://api.javamagazine.com.br/clientes/0pwnanjz"},
{"href": "https://api.javamagazine.com.br/clientes/b02ksdfj"}
]
}
Deve-se reparar nessas listagens que os hyperlinks retornados na resposta não possuem o parâmetro “limit” devido ao fato de estar usando o limite padrão de 10. Se, na primeira requisição, o cliente especificasse o limite a ser utilizado, esses hyperlinks deveriam ser calculados utilizando-se esse limite. Também é importante notar que é livre ao desenvolvedor retornar mais metadados, como a quantidade total de itens, por exemplo.
Essa abordagem nos traz ótimos resultados. O primeiro deles é reduzir a quantidade de dados trafegados pela rede de uma só vez, fazendo com que o cliente seja mais responsivo a seus usuários.
Além disso, os hyperlinks retornados trazem a possibilidade de criação de implementações mais inteligentes do lado do cliente. Estes poderiam utilizar essas URLs retornadas para fazer a paginação, ao invés de ficar construindo as próximas URLs necessárias.
Como podemos ver até agora, deve-se manter duas coisas principais em mente ao projetar uma API: HATEOAS e tentar reduzir ao máximo o tamanho das respostas, de forma que elas retornem apenas o necessário ao cliente e que não atrasem muito a comunicação. Contudo, além disso, ainda são necessárias algumas outras práticas para criar uma boa API e continuaremos abordando-as a seguir.
Respostas de erro
Muitos fatores podem causar um erro na utilização de uma API. Entre eles, estão erros de validação de um recurso submetido, requisição inconsistente enviada pelo cliente, conflito de informações, além de vários outros. Alguns deles podem ser causados pela má utilização da API, enquanto outros podem ser erros comuns que a aplicação cliente deverá estar preparada para tratar, como erros de validação.
Em ambos os casos, a API deve retornar respostas de erro consistentes, que permitam um tratamento para a aplicação cliente e ao mesmo tempo contenham uma explicação direcionada ao desenvolvedor.
Com esses requisitos, faz-se importante as seguintes informações em uma resposta de erro: Status HTTP que descreva o que aconteceu de errado, um código que identifique unicamente o erro, informações pertinentes a esse erro, uma mensagem que poderá ser mostrada ao usuário da aplicação cliente (caso desejado), uma mensagem mais explicativa destinada ao desenvolvedor da aplicação cliente e uma URL que aponte para a documentação desse erro.
Para se obter tanta informação, é necessário começar catalogando os possíveis erros que poderão acontecer na API. Esse catálogo de erros deverá possuir erros identificados por um código e estar presente na documentação da API. Através dessa documentação, o desenvolvedor da aplicação cliente entenderá quais erros deverão ser tratados e preparará sua aplicação para utilizar os códigos de erro na identificação e tratamento dos mesmos.
Para exemplificar, veremos na Listagem 17 o resultado de uma requisição que tenta cadastrar um cliente. Nesse caso, a aplicação cliente submeterá um cliente cujo CPF já foi cadastrado anteriormente, então acontecerá um erro de validação por conflito de CPFs.
POST https://api.javamagazine.com.br/clientes
Resposta:
409 Conflict
{
"status": 409,
"code": 145,
"property": "cpf",
"message": "Um cliente com CPF '43819274830' já existe.",
"developerMessage": "Um cliente com CPF '43819274830' já existe. Verifique se essa requisição não foi enviada anteriormente por acidente.",
"moreInfo": "https://developers.javamagazine.com.br/docs/api/errors/145"
}
Nessa listagem, estamos considerando que o código 145 localiza unicamente o erro de conflito entre campos que devem possuir valores únicos no sistema. Para que esse tipo de erro seja tratado, é importante retornarmos qual o campo em que tal erro aconteceu. No cliente, por exemplo, poderíamos ter um conflito entre os campos únicos de CPF e e-mail. Também podemos observar que existem duas mensagens.
Uma destinada ao usuário comum, que poderá ser exibida pela aplicação cliente (se desejado) e uma destinada ao desenvolvedor, que tenta explicar uma possível causa do erro. Além disso, é retornada a URL que localiza a documentação desse erro, onde é possível encontrar mais detalhes sobre o mesmo.
O ponto mais importante aqui é o código que localiza o erro. Sem ele, um sistema não poderá fazer nenhum tratamento especial para o erro que aconteceu. Portanto, é muito importante catalogar cada um dos erros possíveis na API para trazer uma boa experiência aos desenvolvedores das aplicações clientes da mesma, ajudando-os a satisfazer os usuários finais.
Segurança
Quando falamos da segurança de uma API, temos que ter algumas coisas em mente. A primeira delas é que, por se tratar de REST, a API é stateless. Isso significa que o usuário deverá ser autenticado em cada requisição para que seja verificado se o mesmo possui autorização para acessar determinado recurso. Com isso, o envio de login e senha em cada requisição se torna claramente uma péssima opção.
Para contornar isso, o usuário passará por um login seguro através de nosso serviço, ao invés de submeter o login e senha ao cliente. Depois disso, nosso serviço o redireciona para a aplicação cliente disponibilizando uma chave para a mesma. Com a posse dessa chave, a aplicação autenticará o usuário em cada requisição futura.
Existem várias vantagens para essa abordagem. Uma delas é a exposição limitada. Para entendê-la, imagine que um usuário de nosso site utiliza uma aplicação cliente de nossa API. Quando a aplicação redirecionar o usuário para que ele seja autenticado em nosso site, essa aplicação obterá a chave a ser utilizada para agir como determinado usuário.
Por não expormos o login e a senha do usuário para essa aplicação, visto que o usuário se autenticará através do site ou alguma forma de biblioteca (SDK) disponibilizado por nosso serviço, podemos limitar o acesso dessa aplicação.
Assim, poderíamos deixar a cargo do usuário decidir quais permissões tal aplicação possui sobre seus dados, sendo que as permissões seriam pedidas no momento do login.
Um exemplo clássico disso é o Facebook. Comumente, várias aplicações o utilizam como uma forma de autenticação e também para poder publicar em nome dos usuários. Quando o usuário tenta acessar pela primeira vez tais aplicações, elas o redirecionam para uma página de login e autorização do Facebook.
Nela, o usuário verifica quais permissões a aplicação está pedindo para acessar de sua conta e autoriza o acesso. A partir daí, a aplicação obtém uma chave que será utilizada quando for acessar a API do Facebook em nome do usuário.
Outra vantagem dessa abordagem é a rastreabilidade. Como as aplicações terão que utilizar essa chave, a qual é criada para cada uma, pode-se rastrear as ações de cada aplicação cliente em nome de determinado usuário. Isso pode ser importante para o caso delas estarem utilizando indevidamente o acesso obtido em nome do usuário.
Também é muito útil no caso de uma alteração da senha do usuário. Nesse caso, se a aplicação cliente dependesse do login e da senha, ela perderia o acesso quando o usuário alterasse sua senha. Isso não acontece com o uso de uma chave para cada aplicação cliente.
Com todas essas vantagens, a escolha natural na autenticação é o uso de uma chave passada para o cliente. Mas que caminho podemos seguir na implementação de tal funcionalidade? Para isso, existem diversos padrões de segurança conhecidos. Portanto, ao invés de criarmos um novo esquema de segurança, devemos seguir um desses padrões.
O padrão mais conhecido e mais utilizado pelas APIs de grandes empresas é o OAuth. Atualmente, ele está na versão 2.0, mas ainda existem grandes APIs utilizando sua versão 1.0a. Por se tratar de um padrão, geralmente temos que entendê-lo e implementá-lo. Porém, existem algumas bibliotecas que ajudam na implementação, apesar de não tirar a necessidade do entendimento do mesmo.
Por ser muito extenso, o estudo sobre OAuth será deixado para outro momento. Antes de finalizar o tópico de segurança, no entanto, precisamos nos atentar a mais um detalhe: a autorização não deve ser baseada em URLs, mas sim em recursos.
Isso é bem simples de se entender, afinal, a API é construída a partir de recursos. Então, por mais que seja tentador simplificar o esquema de autorização através de URLs para simplificar a implementação, deve-se fazê-lo sobre recursos, para se evitar falhas de segurança.
Cache através de ETag
Para possibilitarmos o cache por parte da aplicação cliente, podemos adotar um esquema que utiliza o header HTTP ETag. Tal implementação é feita de forma que, quando a aplicação cliente requisitar determinado recurso pela primeira vez, o servidor deverá retornar esse header com um valor que identifique o estado atual do recurso. Em uma requisição posterior, o cliente passará esse valor de volta ao servidor, como uma forma de avisar que já possui o recurso em cache naquele estado. Assim, se não houver mudanças no estado do recurso, o servidor apenas avisa o cliente de que poderá continuar usando tal versão do recurso. Caso contrário, o servidor responde com a nova versão e um novo valor no ETag.
Para deixar mais claro, podemos imaginar que haja um controle de versão no servidor, de tal forma que a cada modificação de um recurso, o valor da versão é incrementado. Assim, pode-se utilizar esse valor da versão para se criar um conjunto de caracteres que representem o estado atual do recurso.
No caso de uma requisição por múltiplos recursos, podemos combinar as versões dos mesmos para gerar o conjunto de caracteres passado na ETag.
A Listagem 18 mostra como um recurso do tipo Cliente é retornado como resposta para uma requisição, em conjunto com sua ETag. Enquanto isso, a Listagem 19 mostra a próxima requisição da aplicação cliente, a qual já possui em cache a versão retornada anteriormente e envia ao servidor o valor que representa tal versão.
Nessa listagem, ainda não houve uma atualização do recurso, então o servidor responde com o status 304 Not Modified. Já na Listagem 20, em uma nova requisição, o recurso já foi modificado, então o servidor retorna a representação dele em conjunto com a nova ETag.
GET https://api.javamagazine.com.br/clientes/2g41s
Headers da resposta:
ETag: “62wsc482nsadf742f7831”
Corpo da resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"idade": 22,
"telefones": ["6298350912", "6232903845"],
"email": "fernando.camargo.ti@gmail.com",
"enderecos": [{
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”
},{
"href": “https://api.javamagazine.com.br/enderecos/7hf852”
}]
}
GET https://api.javamagazine.com.br/clientes/2g41s
Headers da requisição:
If-None-Match: “62wsc482nsadf742f7831”
Resposta:
304 Not Modified
GET https://api.javamagazine.com.br/clientes/2g41s
Headers da requisição:
If-None-Match: “62wsc482nsadf742f7831”
Headers da resposta:
ETag: “2tfh892fds982nksaf8932”
Corpo da resposta:
{
"href": “https://api.javamagazine.com.br/clientes/2g41s”,
"nome": "Fernando Camargo",
"cpf": "62854828532",
"idade": 23,
"telefones": ["6298350912"],
"email": "fernando.camargo.ti@gmail.com",
"enderecos": [{
"href": “https://api.javamagazine.com.br/enderecos/8502hgo”
},{
"href": “https://api.javamagazine.com.br/enderecos/7hf852”
}]
}
Nessas listagens, podemos perceber a interação da aplicação cliente com o servidor. Nessa interação, verificamos três pontos importantes: o uso do header ETag na resposta do servidor, o uso do header If-None-Match na requisição do cliente e o status 304 Not Modified retornado pelo servidor. Em cima desses três pontos, podemos construir uma boa solução que permite a utilização de cache pela aplicação cliente.
A grande vantagem disso é o baixo consumo da rede. Pelo fato do servidor responder com um simples status HTTP, bastante rede é poupada e a requisição se torna mais rápida. Com essa abordagem, também é possível que a aplicação cliente apresente o recurso em cache e verifique por atualizações em plano de fundo. Enfim, essa é uma funcionalidade muito importante que pode ser feita de uma maneira fácil. Portanto, não há porque não fazê-la.
Conclusão
Nesta primeira parte do artigo, vimos os conceitos básicos do estilo arquitetural que vem ganhando espaço na implementação de web services. Também foram mostradas as principais diferenças entre um web service REST e um SOAP, destacando as vantagens do REST para a criação de uma API destinada a aplicações clientes mantidas por terceiros.
Depois desse aprendizado sobre as regras do REST e a justificativa de porque utilizá-lo, vimos as principais boas práticas que devemos seguir para implementarmos uma boa RESTful API. Através dessas boas práticas, podemos criar uma API que não só trará grande produtividade aos desenvolvedores das aplicações clientes, mas também lhes trará grande flexibilidade, além de melhorar o desempenho do servidor e ter sua manutenção facilitada.
Na segunda parte, será mostrado os conceitos por trás do OAuth 2.0 e também um exemplo utilizando as boas práticas através da especificação JAX-RS em conjunto com a implementação Apache CXF, que já possui suporte à criação da autenticação com o padrão OAuth 2.0.
Construindo uma RESTful API – Parte 2
Este artigo finaliza o estudo sobre o desenvolvimento de uma RESTful API que foi introduzido na primeira parte do mesmo. No artigo anterior, foram apresentadas as diferenças entre alguns tipos de web service, entre eles o estilo REST.
Além disso, o leitor viu boas práticas que vêm sendo adotadas para tornar uma API nesse estilo a mais completa e intuitiva possível. Aqui, aplicaremos alguns desses conceitos na prática através da implementação passo a passo de uma API simples, mas que mostra um pouco sobre como seria uma API no mundo real.
Dito isso, pode-se concluir que este tema é útil para desenvolvedores que precisam criar uma API pública ou privada para expor dados e funcionalidades de um sistema web para outras aplicações, sejam elas web, mobile ou desktop.
O REST é um estilo arquitetural para construção de web services. Ele vem sendo largamente adotado por diversos serviços devido à sua simplicidade, o que o torna mais atraente a desenvolvedores utilizadores de tais web services.
Diferente do SOAP, o consumidor desse tipo de serviço não precisa de bibliotecas especiais. Ao invés disso, apenas de requisições HTTP são necessárias. Isso torna o REST muito mais simples e intuitivo.
Esse estilo de construção de web service foi apresentado na primeira parte deste artigo, onde também foram vistas as boas práticas a serem empregadas em sua implementação.
Para entender melhor o tema, veremos um exemplo que utiliza REST em conjunto com suas melhores práticas para criar uma API simples que envolve usuários, postagens e comentários. Ao final desse passo a passo, o leitor estará hábil a desenvolver APIs intuitivas e completas através da aplicação do que for aprendido.
Para tal exemplo, será utilizado o Spring como container de nossa aplicação e a especificação JAX-RS, uma especificação criada para desenvolvermos serviços REST.
Como biblioteca que implementa a JAX-RS, utilizaremos a Apache CXF. O motivo para isto é que ela inclui alguns módulos que poderão facilitar o desenvolvimento completo de uma API. Entre eles, podemos destacar um módulo que facilita a implementação do padrão de segurança OAuth.
Para não tornar o exemplo muito complexo, vamos deixar de fora a parte de segurança da API. Para concluir essa parte, recomenda-se o estudo do OAuth, bem como do módulo do Apache CXF que facilita sua implementação. Além disso, pode-se utilizar outros frameworks de segurança em conjunto com o OAuth para fazer o controle de permissões, como o Apache Shiro ou o Spring Security.
Aplicação prática da API REST
A aplicação utilizada como exemplo será bem simples: um sistema de postagens e comentários. Nesse sistema, um usuário poderá criar uma postagem, a qual iria para sua linha do tempo, como em uma rede social, e outros usuários poderão comentar essa postagem.
Essa aplicação não possuirá tela, o que significa que toda a interação será feita a partir da API. Em uma situação do dia a dia, tal API poderia ser utilizada por uma aplicação cliente, responsável por fazer a apresentação ao usuário.
Para começarmos a implementação, o primeiro passo é modelar a interação da API, definindo as URLs e os métodos disponíveis. Como esperado, existirão três raízes onde se localizarão os três recursos disponíveis: usuários, postagens e comentários.
Além das URLs que usam basicamente a raiz de um recurso para interagir com ele, ainda existem outras para interagir com recursos relacionados. Entre elas, serão necessárias URLs para acessar os recursos pertencentes ao usuário: suas postagens e seus comentários.
Ainda será preciso uma forma de acessar os comentários de uma postagem específica. A partir dessa análise, definimos as seguintes URLs:
- /users: URL raiz para acessar os recursos dos usuários. Inclui métodos de criação e obtenção da lista de usuários;
- /users/{id}: URL para acessar um usuário específico. Inclui métodos de modificação, obtenção e deleção do usuário;
- /posts: URL raiz para acessar os recursos das postagens. Inclui métodos para criação e obtenção da lista geral de postagens;
- /posts/{id}: URL para acessar um post específico. Inclui métodos para modificação, obtenção da postagem e deleção da mesma;
- /comments/{id}: URL para acessar um comentário específico. Por ser um recurso muito ligado à postagem, não faria sentido haver métodos de listagem geral e criação de forma desvinculada. Por isso, permitiremos listagem de comentários de uma postagem específica e criação dos mesmos de forma vinculada a uma postagem. Esses métodos serão vistos em outras URLs. Essa inclui apenas métodos de modificação e deleção;
- /users/{id}/posts: URL para acessar as postagens de um usuário específico. Inclui apenas o método de listagem das postagens de tal usuário;
- /users/{id}/comments: URL para acessar os comentários feitos por determinado usuário nas mais diversas postagens. Também disponibiliza apenas um método de listagem;
- /posts/{id}/comments: URL para acessar os comentários de um determinado post. Além de incluir um método de listagem dos mesmos, também inclui um método de criação de novos comentários ligados à postagem.
Conforme apresentado na primeira parte deste artigo, há um mapeamento de cada método HTTP para sua respectiva ação em uma API REST. Aqui, utilizaremos: GET para obter, POST para criar, DELETE para deletar e PUT para edições parciais.
Deixaremos POST para edições completas de lado, mas poderíamos incluí-lo, se desejássemos.
Outro detalhe a especificarmos é sobre o autor das ações. Vamos considerar aqui que existe uma implementação de segurança que identifica o usuário que está executando determinada requisição e esse será autor dos recursos criados que possuam um campo de autor.
Ou seja, ao criar uma postagem, o autor da mesma será o usuário autenticado no momento da requisição. Porém, para simplificar o exemplo, não faremos tal implementação de segurança. Ao invés disso, vamos simular sua existência através da criação de um método que sempre retornará um usuário padrão, como se ele houvesse sido autenticado.
Para finalizar essa pequena especificação da API, precisamos identificar a estrutura dos recursos, ou seja, quais os campos existentes em cada um deles. As Listagens 1 a 3 mostram os JSONs a serem submetidos na criação dos recursos de usuário, postagem e comentário, respectivamente. Enquanto isso, as Listagens 4 a 6 mostram os JSONs de listagem desses recursos.
{
"name": "Default",
"login": "Default",
"email": "default@default.com",
"password": "default"
}
{
"title": "Title",
"text": "Long text"
}
{
"text": "Long text"
}
{
"href":"http://localhost:8080/restfulapi/api/users",
"offset":0,
"limit":25,
"items":[
{
"href":"http://localhost:8080/restfulapi/api/users/ff808181436254ac01436255c5ea0000",
"name":"Default",
"login":"Default",
"email":"default@default.com"
}
]
}
{
"href":"http://localhost:8080/restfulapi/api/posts",
"offset":0,
"limit":25,
"items":[
{
"href":"http://localhost:8080/restfulapi/api/posts/ff808181436254ac01436255c6340001",
"author":{
"href":"http://localhost:8080/restfulapi/api/users/ff808181436254ac01436255c5ea0000"
},
"title":"Title",
"text":"Text",
"dateCreated":"2014-01-05T12:17:005Z"
}
]
}
{
"href":"http://localhost:8080/restfulapi/api/posts/ff808181436254ac01436255c6340001/comments",
"offset":0,
"limit":25,
"items":[
{
"href":"http://localhost:8080/restfulapi/api/comments/ff808181436254ac01436265f6c60002",
"author":{
"href":"http://localhost:8080/restfulapi/api/users/ff808181436254ac01436255c5ea0000"
},
"text":"Text",
"dateCreated":"2014-01-05T12:34:046Z"
}
]
}
A primeira coisa a se reparar nas listagens que mostram os JSONs das listagens dos recursos é a presença constante do atributo href. Como foi visto na parte 1 deste artigo, esse atributo é importante para promover o conceito de HATEOAS.
Podemos notar também que tanto o autor da postagem quanto o do comentário têm apenas uma URL retornada nas listagens desses recursos. Outro atributo a ser notado é o dateCreated. Como podemos verificar, ele segue a formatação ISO 8601.
Nessas listagens podemos sentir falta, ainda, de atributos que tornem mais avançado o uso do HATEOAS. Poderíamos incluir campos com links para a próxima página, a página anterior, a primeira página e a última página.
Assim, facilitaríamos a paginação. Esse e outros detalhes ficarão como atividade a ser feita pelo leitor ao final do artigo.
Com a API bem especificada, podemos começar a implementá-la utilizando as boas práticas vistas até agora. Para agilizar essa implementação, este artigo inclui um projeto em estado inicial que já contém as classes utilitárias.
Então, a primeiro momento, estudaremos o que já foi implementado e depois criaremos a API na forma de um passo a passo.
Entendendo o pacote inicial
Antes de vermos os utilitários criados para a implementação da API, veremos a estrutura básica do projeto. Podemos começar identificando que o mesmo foi construído utilizando o Maven como ferramenta de build.
Também foi utilizado JPA com Hibernate para a persistência de dados e o Spring como container de injeção de dependências. Para agilizar a construção do projeto, foi utilizado o Spring Roo, que já cria automaticamente algumas operações nas entidades. Para facilitar ao leitor, o Spring Roo foi removido do projeto, ficando apenas o que foi criado por ele.
É importante notar que essa ferramenta utiliza o padrão de Domain Driven Design, que faz com que as classes de domínio (entidades) contenham lógica ao invés de apenas carregar dados. Portanto, os métodos de persistência estão dentro de tais classes.
Como dito anteriormente, utilizaremos a especificação JAX-RS com a implementação do Apache CXF para criarmos a API. Existem várias outras opções que implementam essa especificação e também outras que não a seguem.
Ao criar uma API, o desenvolvedor deve analisar as vantagens e desvantagens de cada framework, também considerando sua experiência com os frameworks disponíveis. Por motivos explicados anteriormente, utilizaremos o Apache CXF.
Com o conjunto de frameworks definido para o projeto, podemos entender os arquivos de configuração e os pacotes contidos no mesmo. Para começar, podemos ver as configurações comuns do Spring e do JPA, que configuram um banco de dados em arquivo para facilitar a execução do exemplo. Além dos arquivos que configuram o Spring e o JPA, também vemos outro diferente, chamado restErrors.properties.
Esse é um arquivo de propriedades que mapeia cada exceção lançada para: um status HTTP, um código de erro, uma mensagem ao usuário, uma mensagem ao desenvolvedor e uma URL para mais informações sobre o erro. Esse detalhamento do erro foi discutido na primeira parte deste artigo.
Muito do que é encontrado nesse pacote inicial, incluindo esse arquivo que mapeia as exceções para as propriedades de erro, foram retirados do exemplo de API publicado por Les Hazlewood, CEO da Stormpath.
Em seu exemplo original, ele utilizou o Jersey como implementação do JAX-RS e incluiu uma única entidade de tarefa, não possuindo entidades compostas ou situações mais complexas. Será explicado agora algumas das classes incluídas por ele que foram mantidas e adaptadas para esse exemplo. Para obter o exemplo original, procure-o na seção Links deste artigo.
Antes de vermos mais informações sobre o código de exemplo inicial, precisamos entender a estrutura geral da aplicação. Para isso, a Figura 1 mostra a estrutura de pacotes do projeto com o nome que utilizaremos para identificar cada um deles. Aos poucos, exploraremos cada um desses pacotes, vendo detalhes sobre as classes incluídas neles.
Para conhecermos um pouco do pacote inicial, podemos começar pelo pacote de modelo de negócio. Nele está contida a interface Entity, que simplesmente possui um getter para a propriedade a ser incluída em todas as entidades: a propriedade “id”, que é basicamente o identificador da entidade. Essa é a interface base de todas as classes de domínio.
Seu objetivo é criar uma forma genérica de referenciar instâncias de classes de domínio, incluindo um método para obter o identificador de tais instâncias. As demais classes desse pacote são as classes de domínio, que representam nossas entidades.
Ao observá-las, pode-se notar que todas elas implementam a interface Entity e possuem métodos de persistência.
No pacote de utilitários, podemos encontrar diversas classes utilitárias usadas em toda a aplicação. Entre elas, existem classes usadas para reflection, comparações de objetos, métodos para manipulação de Strings, entre outras funções.
Para simular a existência de um meio de autenticação para os usuários da API, o pacote de serviços possui apenas a classe UsuarioService, que apresenta um único método que simula a obtenção do usuário que está interagindo no momento com a API: getCurrentUser(). Verificando o código, observamos que, para simular a obtenção do usuário autenticado, esse método analisa se um usuário padrão foi criado anteriormente e o retorna em caso positivo.
Caso contrário, ele cria esse usuário, o armazena no banco de dados e o retorna. Dessa maneira, após a primeira invocação, esse método sempre retornará o mesmo usuário.
Continuando no estudo dos pacotes, devemos observar o pacote mais importante: rest. Podemos começar a explorá-lo a partir da classe que configura o Apache CXF, a JaxRsServerConfig. Para deixar claro, é possível configurar o servidor do Apache CXF para JAX-RS através de um arquivo XML para Spring.
Porém, adotando esse estilo de configuração, é necessário inserir cada um dos controladores dos recursos manualmente nesse XML, incluindo também as suas dependências. Para evitar inserir tudo isso via XML e utilizar annotations no lugar, foi criada essa classe que configura o servidor do Apache CXF e procura por classes anotadas com @Path, que é a anotação básica a ser utilizada no JAX-RS.
Para que essa configuração do Apache CXF funcione, precisamos primeiro incluir seu Servlet no web.xml. Nesse arquivo notamos a presença do CXFServlet, que é mapeado para o caminho /api/*. Para concluir, o Spring está configurado para encontrar classes através de suas anotações clássicas.
Por isso, anotamos a JaxRsServerConfig com a anotação @Configuration, para que ela seja utilizada no momento da inicialização do Spring. Nessa classe também vemos a anotação @ComponentScan, que especifica o pacote dos controladores do JAX-RS como pacote de varredura para busca de componentes.
Quando o Spring acionar nossa configuração, ele automaticamente passará os componentes encontrados nesse pacote para nossa classe.
Ainda em JaxRsServerConfig, devemos entender especificamente o método jaxRsServer(), que é o responsável pela construção do servidor do Apache CXF. Seu primeiro bloco de código faz uma iteração pelos componentes obtidos pelo Spring no pacote dos controladores e verifica se eles possuem a anotação @Path. Para cada componente encontrado, cria-se um ResourceProvider responsável por criar instâncias dos controladores e ao fim adiciona-o à lista de provedores de recurso do servidor do Apache CXF.
Em seguida, esse método cria a fábrica que construirá tal servidor e faz determinadas configurações pertinentes à nossa API. Uma delas é a configuração do provedor de JSON, que será o responsável por mapear os objetos para respostas em formato JSON.
Para isso, utiliza-se a famosa biblioteca Jackson. Nessa mesma configuração, especifica-se que as datas deverão ser formatadas utilizando o padrão ISO 8601.
Para finalizar, adicionamos um mapeador de exceções ao servidor. Esse mapeador é a classe DefaultExceptionMapper, que é responsável pela leitura do arquivo restErrors.properties, visto anteriormente, para que as exceções lançadas sejam mapeadas para respostas de acordo com as configurações desse arquivo.
Com isso, concluímos a configuração do servidor JAX-RS do Apache CXF e podemos começar a criar controladores a serem incluídos no pacote controller. Mas antes disso, vamos continuar focados no mapeamento das exceções e analisar a classe RestError.
Uma instância dessa classe é construída pelo DefaultExceptionMapper e retornada como resposta quando uma exceção é lançada. Como nossa API utilizará o JSON como formato para as respostas, uma representação no formato de JSON será criada, contendo as propriedades vistas anteriormente que descrevem um erro na API.
Ainda falando sobre o tratamento de erros, podemos ver uma exceção previamente criada no pacote exception. Essa será lançada quando um recurso não for encontrado em uma requisição que especifique seu identificador. Ou seja, em caso de recursos não encontrados, lançaremos a UnknownResourceException.
Esta exceção já está configurada no arquivo restErrors.properties para retornar o status HTTP 404, seguindo o padrão de respostas do protocolo HTTP.
Para facilitar a utilização dos status HTTP, existe o enum HttpStatus no pacote http, que contém todos os possíveis status com seu nome e número. Esse enum já é utilizado na classe RestError e poderá ser empregado nos controladores a serem criados.
Enfim, podemos estudar as classes mais importantes: aquelas contidas no pacote resource. A primeira classe a ser visualizada é a classe Link, que deverá ser superclasse de todos os recursos, por possuir definições comuns a todos eles.
Observando seu código, podemos ver que ela estende de LinkedHashMap, ou seja, é um mapa de propriedades. Ela também define algumas constantes para URLs e possui construtores que recebem uma Entity e adicionam a propriedade href.
Para compreender o uso dessa classe, precisamos entender como funciona o mapeador que transforma um objeto retornado como resposta em JSON. Tal mapeador lê as propriedades de nosso objeto e cria um JSON, que é o resultado final enviado ao cliente da API.
Isso significa que se usarmos nossas entidades como resposta, tendo um JSON representando-as diretamente, acabaria por expô-las aos clientes e passaríamos por diversas complicações para configurar quais propriedades devem ou não estar presentes no JSON final a cada requisição.
Um detalhe importante é que esse mapeador também funciona com classes que implementam a interface Map. Nesse caso, ele obtém as chaves presentes no mapa de propriedades e cria o JSON a partir delas. Caso uma dessas chaves possua outro mapa como valor, o mapeador criará um JSON aninhado fazendo o mesmo processo com esse mapa.
Vale lembrar que podemos representar qualquer objeto ou entidade através de mapas aninhados. Então, para evitar a exposição direta de nossas entidades e controlar facilmente quais propriedades serão expostas, criamos mapas que representam nossas entidades.
Nesse contexto entra a classe Link. Como foi dito, ela é um mapa de propriedade e será superclasse de todos os nossos recursos. Essa classe, para respeitar o HATEOAS, insere imediatamente a propriedade href em sua construção.
Ela poderá, então, ser usada de duas formas: através da criação de subclasses para cada recurso da API e também pela criação de instâncias diretas quando quisermos retornar apenas um link para determinado recurso. Para cada subclasse criada a partir dela, ou seja, para cada novo recurso, adiciona-se uma constante nela com o caminho raiz de tal recurso.
Nesse caso, possuímos constantes para nossos três recursos: usuários, postagens e comentários.
Uma classe importante que herda de Link é a CollectionResource. Esta classe deve ser usada para responder a requisições de listagem de recursos. Ela já está preparada para seguir o conceito de HATEOAS, bem como facilitar a paginação através de um resultado que inclui o offset e o limit utilizados na paginação.
Em sua construção, CollectionResource recebe como parâmetro uma coleção de itens de determinado recurso, a qual é ajustada na propriedade items. Para exemplificar, poderíamos construí-la com uma lista de recursos do tipo usuário. O resultado de uma instância dessa classe já foi mostrado anteriormente. As Listagens 4 a 6 mostram o resultado final para os recursos de usuário, postagem e comentário.
Voltando ao código da classe Link, mais especificamente no método createHref(), podemos ver o uso do enum ResourcePath na construção da URL a ser ajustada como href (hyperlink que localiza um recurso).
Ao observarmos esse enum, notamos que ele mapeia nossas entidades às URLs raiz especificadas nas constantes da classe Link. Além disso, ele possui um método chamado forClass() que retorna uma instância do enum para determinada classe enumerada no mesmo. Assim, o método createHref() pode obter a URL de determinado recurso sem saber previamente de qual entidade ele é, visto que todas as entidades herdam de Entity.
Por fim, resta estudarmos as classes criadas para cada um dos nossos recursos: UserResource, PostResource e CommentResource. Pode-se ver que essas classes definem em constantes os nomes das propriedades expostas por esses recursos. Usamos nomes iguais para as propriedades dos recursos e de suas entidades equivalentes.
Por exemplo, na entidade User, temos a propriedade name, que é equivalente à propriedade de mesmo nome no UserResource. Vale notar que poderíamos utilizar nomes diferentes para as propriedades expostas nos recursos ou até mesmo criar propriedades compostas nos mesmos, como seria o caso de juntar o primeiro e o último nome do usuário para compor uma única propriedade com o nome do mesmo.
Esse poder nos traz um nível bom de encapsulamento de nossas entidades, evitando a exposição direta delas aos nossos clientes.
Observando o código das classes que representam os recursos, podemos notar que elas possuem a mesma estrutura básica. A primeira coisa a ser vista é a semelhança entre seus construtores. Todas possuem construtores que recebem os mesmos parâmetros: a entidade a qual o recurso representa, uma coleção de campos a serem expostos (fields) e uma coleção que especifica quais campos devem ser expandidos (expand).
Outro parâmetro que todos recebem é uma instância de UrlInfo, que é injetado pelo JAX-RS e serve para se obter informações sobre a URL utilizada na requisição realizada. Os parâmetros fields e expand do construtor são o que dão a flexibilidade almejada à nossa API.
Através deles, conseguimos criar respostas parciais onde o cliente pode especificar quais campos deverão ser incluídos e quais deverão ser expandidos.
Ainda nos construtores dessas classes, notamos que os parâmetros que especificam os campos a serem incluídos e expandidos possuem valores padrões. Também vemos que os campos que referenciam outros recursos, quando incluídos mas não expandidos, recebem instâncias de Link como valor. Dessa forma, o cliente da API poderá requisitar esse recurso posteriormente.
Com as classes dos recursos estudadas, resta apenas uma única classe: BaseController. Pode-se verificar que ela é muito simples, possuindo apenas um método, chamado created(), que recebe um Link, podendo esse ser uma subclasse de qualquer um dos recursos, e retorna uma resposta com o status 201 Created. Esse método é apenas um utilitário para responder a uma requisição de criação para determinado recurso.
Com isso concluímos o estudo do pacote inicial. A partir de agora, iniciaremos a construção dos controladores que proverão os serviços especificados anteriormente.
Criando o controlador de usuários
O primeiro passo é a criação de uma classe no pacote br.com.devmedia.javamagazine.restfulapi.rest.controller. Essa classe deverá ser anotada com @Path, passando o caminho raiz de usuários como parâmetro. Para isso, podemos utilizar a constante definida na classe Link. Logo, a nova classe criada terá o esqueleto mostrado na Listagem 7. Para facilitar a visualização, os imports não serão incluídos. No geral, as classes do JAX-RS serão importadas do pacote javax.ws.rs.*.
@Path(Link.USERS)
@Component
public class UserController extends BaseController {
@Autowired
private UserService userService;
}
Para quem não está familiarizado com o Spring, as anotações @Component e @Autowired são dele. A primeira torna essa classe um componente controlado pelo container de injeção de dependências. A segunda pede para que uma instância da classe UserService seja injetada como dependência de nosso controlador.
Com essa classe criada, a configuração feita para o Apache CXF estará pronta para reconhecê-la e adicioná-la como um dos provedores de recursos do servidor. Agora necessitamos de métodos que serão invocados de acordo com as requisições HTTP. Para esse recurso, como foi apontado anteriormente, teremos métodos de: listagem, criação, obtenção de usuário específico, atualização e deleção de usuário.
No JAX-RS, para cada requisição a ser tratada, criamos um método na classe do controlador. O método certo para tratar determinada requisição é selecionado de acordo com a configuração feita por annotations, em que se especifica o caminho da requisição, o método HTTP, entre outras possíveis configurações.
Em cada método, podemos especificar subcaminhos ou deixar sem qualquer especificação, o que faz com que ele fique mapeado para o caminho raiz do controlador.
Assim como no controlador, utilizamos a anotação @Path para isso. Para definir o método HTTP que determinado método trata, utilizamos as anotações @GET, @POST, @PUT, @DELETE, @HEAD e @OPTIONS. Em nosso exemplo, não mostraremos o uso dos dois últimos.
Além das anotações que especificam o método HTTP e o caminho da requisição, ainda faremos uso das anotações que indicam o formato da resposta e da requisição.
Eles são especificados pelas anotações @Produces e @Consumes, respectivamente. Elas instruem o Apache CXF a determinar qual adaptador utilizar para fazer a serialização do objeto retornado como resposta para o formato correto e para fazer a desserialização da requisição de determinado formato para um objeto do sistema.
Para nosso exemplo, essas anotações serão suficientes para configurar os nossos métodos.
Outra característica importante do JAX-RS é que podemos incluir parâmetros em nossos métodos em conjunto com anotações que especificam a forma de obtenção dos mesmos. Assim, quando uma requisição chegar, determinados parâmetros da requisição serão extraídos e passados como parâmetros para nossos métodos.
Para utilizar essas funcionalidades, podemos fazer uso de anotações como @QueryParam, @PathParam e @FormParam. Essas anotações especificam de onde serão retirados os parâmetros, podendo esses ser da query-string (exemplo: /users?fields=name), do caminho utilizado na requisição (exemplo: /users/h27sdfjh43, sendo h27sdfjh43 o id do usuário) ou de um formulário. Além disso, pode-se obter um objeto complexo em requisições do tipo POST e PUT.
Nesses casos, não precisamos anotar o parâmetro, apenas adicionamos o objeto como parâmetro e esperamos obtê-lo como corpo da requisição no formato especificado pela anotação @Consume.
Para auxiliar na obtenção dos parâmetros, ainda podemos utilizar a anotação @DefaultValue, que especifica um valor padrão a ser adotado caso o parâmetro não esteja presente. Vale observar que essa anotação recebe uma String como parâmetro.
A última anotação que adotaremos é a @Context. Ela serve para injetarmos informações sobre a requisição atual ou o ambiente JAX-RS. Essas informações incluem instâncias das seguintes classes do JAX-RS: Application, UriInfo, Request, HttpHeaders, SecurityContext e Providers. Em nossa implementação, utilizaremos apenas UriInfo para obter informações sobre a URL requisitada, para que possamos construir nossas hrefs.
Para tornar toda essa teoria mais concreta, a Listagem 8 mostra o primeiro método do controlador de usuário. Esse método retorna a listagem em JSON de forma paginada dos usuários cadastrados no sistema. Deve-se notar o uso de cada uma das anotações citadas até aqui, verificando o parâmetro fields, que especifica os campos de usuário a serem expostos como resposta, bem como o parâmetro expand, que indica se devemos expandir os recursos da listagem, e também os parâmetros utilizados na paginação: offset e limit.
@GET
@Produces(MediaType.APPLICATION_JSON)
public CollectionResource list(@Context UriInfo info, @QueryParam("fields") List<String> fields,
@DefaultValue("false") @QueryParam("expand") boolean expand,
@DefaultValue(CollectionResource.DEFAULT_OFFSET+"")
@QueryParam("offset") int offset,
@DefaultValue(CollectionResource.DEFAULT_LIMIT+"")
@QueryParam("limit") int limit){
List<User> users = User.findUserEntries(offset, limit);
List<Link> userResources = new ArrayList<Link>(users.size());
if(expand){
for(User user : users){
userResources.add(new UserResource(info, user, fields, null));
}
}
else {
for(User user : users){
userResources.add(new Link(info, user));
}
}
return new CollectionResource(info, Link.USERS, userResources);
}
Para relembrar, foi dito na primeira parte deste artigo que a listagem deve, por padrão, retornar apenas hyperlinks dos recursos, dando a opção ao usuário de expandi-los. Podemos ver que o método mostrado na Listagem 8 faz exatamente isso. No caso do parâmetro expand ser false ou não ter sido especificado, retornamos uma CollectionResource apenas com links (instâncias de Link) dos usuários. O resultado disso é que apenas o campo href é retornado em cada usuário. Assim, o cliente poderia requisitar separadamente cada um dos recursos da listagem.
Repare também que a instância de UriInfo é passada para os construtores das subclasses de Link para que os hrefs sejam construídos. No geral, essas subclasses usam o ResourcePath e o identificador do recurso para construir essas hrefs.
Porém, no caso do CollectionResource, não se tem um identificador definido para construir esses hyperlinks, visto que ele é composto por múltiplos recursos, cada um com o seu identificador. Nesse caso, devemos passar como parâmetro o caminho a ser ajustado na propriedade href. Esse caminho é aquele que localiza a coleção de recursos retornada como resposta. Nesse método, é o próprio caminho raiz dos usuários.
Um último detalhe a ser verificado na Listagem 8 é que permitimos apenas a expansão simples dos recursos listados, não dando a opção de expandir os recursos relacionados. O motivo disso é a simplicidade e também evitar respostas muito grandes.
Afinal, tudo bem expandir recursos relacionados quando obtemos um único recurso, mas isso pode começar a ficar custoso quando lidamos com uma lista desses recursos.
Continuando na implementação dos métodos do usuário, devemos codificar agora o método para obter um recurso específico através de seu identificador. Para isso, devemos estudar melhor o uso da anotação @PathParam em conjunto com a @Path. Como vimos anteriormente, a anotação @Path recebe um único valor, uma String, que indica o caminho a ser utilizado na requisição.
Nesse caminho, ainda podemos incluir parâmetros, colocando-os entre colchetes. No caso de nosso método de seleção de usuário, o sub caminho seria: “/{id}”. Juntando com o caminho especificado no controlador, o caminho final a ser requisitado seria: “/users/{id}”.
Podemos notar que esses parâmetros são nomeados, pois é através do nome que obtemos o valor de um parâmetro. Para tal, anotamos um dos parâmetros de nosso método com @PathParam incluindo o nome do parâmetro especificado na anotação @Param. Isso tudo é usado no método de obtenção de um usuário pelo identificador, o qual inclui a anotação @Param(“id”) para obter o parâmetro necessário. O código completo desse método pode ser visto na Listagem 9.
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public UserResource getUser(@Context UriInfo info, @PathParam("id") String id,
@QueryParam("fields") List fields,
@QueryParam("expand") List expand){
User user = User.findUser(id);
if(user == null){
throw new UnknownResourceException();
}
return new UserResource(info, user, fields, expand);
}
Para facilitar a implementação, vamos receber como parâmetro uma instância da classe User. Apesar dessa abordagem utilizada aqui, recomenda-se receber como parâmetro outro objeto ou uma instância de Map, para que não exponhamos diretamente nossa entidade de usuário. A Listagem 10 mostra a implementação desse método.
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(@Context UriInfo info, User user){
user.persist();
UserResource userResource = new UserResource(info, user);
return created(userResource);
}
Nessa listagem, deve-se notar a presença da anotação @Consumes, que especifica o formato dos dados recebidos no corpo da requisição. Também podemos ver que esse método poderia ser melhorado através de uma validação explícita dos dados submetidos, criando exceções a serem mapeadas para respostas que indiquem bem os erros causados. Além disso, como dito anteriormente, seria mais recomendado receber algo como um Map para desacoplarmos nossa API de nossas entidades internas. Essas melhorias não foram feitas aqui para poupar espaço e deixar o código mais simples, mas deve-se manter em mente que elas são necessárias em uma API a ser colocada em produção.
Com os métodos de listagem, obtenção e criação de usuários, resta implementarmos o método de modificação e deleção. Faremos primeiro o método de modificação.
Nesse, mostraremos a possibilidade de obtenção de um mapa como parâmetro, mas utilizaremos um utilitário da biblioteca BeanUtils para extrair os valores do mapa para uma instância de User. Novamente, o objetivo aqui é tornar o código mais simples e curto. Esse novo método pode ser visualizado na Listagem 11.
@Path("/{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response updateUser(@Context UriInfo info, @PathParam("id")
String id, Map properties)
throws IllegalAccessException, InvocationTargetException {
User user = User.findUser(id);
if(user == null){
throw new UnknownResourceException();
}
BeanUtils.populate(user, properties);
user.merge();
return Response.ok(new UserResource(info, user),
MediaType.APPLICATION_JSON).build();
}
Como pode ser observado, a implementação desse método é bem simples. Primeiro, tenta-se localizar o usuário pelo identificador passado como parâmetro através da URL. Depois, obtém-se um mapa com os valores recebidos como parâmetro em um JSON e atualiza-se o usuário com os valores recebidos no mapa. Por fim, respondemos com o status HTPP “201 OK”, incluindo o usuário (em seu novo estado) no corpo da resposta.
Ainda mais simples é a implementação do método de deleção dos usuários, que pode ser visto na Listagem 12. Ele apenas tenta localizar o usuário através do identificador e tenta removê-lo caso exista.
@Path("/{id}")
@DELETE
public void deleteUser(@PathParam("id") String id){
User user = User.findUser(id);
if(user == null){
throw new UnknownResourceException();
}
user.remove();
}
Com isso, terminamos o controlador de usuários. Porém, deve-se observar que ainda não há qualquer controle de segurança, e deste modo qualquer usuário poderia deletar outro, por exemplo.
Em uma API colocada em produção, qualquer pessoa poderia criar um usuário, obter informações dos existentes e até atualizar seu próprio registro, mas apenas aquelas com direitos especiais poderiam deletar e modificar qualquer usuário. Como já informado, a tarefa de implementar a segurança ficará para o leitor.
Implementando os demais controladores
Com o fim da implementação do controlador de usuários, podemos prosseguir com a implementação dos controladores dos demais recursos. No geral, não haverá nada novo nesses controladores, pois a lógica será a mesma e o código bem parecido.
A única diferença nos controladores de postagem e de comentários é que eles farão uso do UserService para obter o usuário atual, o ajustando como autor dos recursos criados. Além disso, também será ajustado o valor do campo dateCreated com a data de criação desses recursos. Por ter tão poucas diferenças, o código completo das duas novas classes está presente nas Listagens 13 e 14.
@Path(Link.POSTS)
@Component
public class PostController extends BaseController {
@Autowired
private UserService userService;
@GET
@Produces(MediaType.APPLICATION_JSON)
public CollectionResource list(@Context UriInfo info, @QueryParam("fields") List<String> fields,
@DefaultValue("false") @QueryParam("expand") boolean expand,
@DefaultValue(CollectionResource.DEFAULT_OFFSET + "")
@QueryParam("offset") int offset,
@DefaultValue(CollectionResource.DEFAULT_LIMIT + "")
@QueryParam("limit") int limit) {
List<Post> posts = Post.findPostEntries(offset, limit);
List<Link> postResources = new ArrayList<Link>(posts.size());
if (expand) {
for (Post post : posts) {
postResources.add(new PostResource(info, post, fields, null));
}
} else {
for (Post post : posts) {
postResources.add(new Link(info, post));
}
}
return new CollectionResource(info, Link.POSTS, postResources);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(@Context UriInfo info, Post post) {
post.setAuthor(userService.getCurrentUser());
post.setDateCreated(new Date());
post.persist();
PostResource postResource = new PostResource(info, post);
return created(postResource);
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public PostResource getPost(@Context UriInfo info, @PathParam("id") String id,
@QueryParam("fields") List<String> fields,
@QueryParam("expand") List<String> expand) {
Post post = Post.findPost(id);
if (post == null) {
throw new UnknownResourceException();
}
PostResource postResource = new PostResource(info, post, fields, expand);
postResource.setUserService(userService);
return postResource;
}
@Path("/{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response updatePost(@Context UriInfo info,
@PathParam("id") String id, Map properties) {
Post post = Post.findPost(id);
if (post == null) {
throw new UnknownResourceException();
}
if(!post.getAuthor().equals(userService.getCurrentUser())){
throw new OnlyPostsCreatorCanModifyException();
}
try {
BeanUtils.populate(post, properties);
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
post.merge();
return Response.ok(new PostResource(info, post),
MediaType.APPLICATION_JSON).build();
}
@Path("/{id}")
@DELETE
public void deleteUser(@PathParam("id") String id){
Post post = Post.findPost(id);
if (post == null) {
throw new UnknownResourceException();
}
if(!post.getAuthor().equals(userService.getCurrentUser())){
throw new OnlyPostsCreatorCanDeleteException();
}
post.remove();
}
}
@Path(Link.COMMENTS)
@Component
public class CommentController extends BaseController {
@Autowired
private UserService userService;
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public CommentResource getComment(@Context UriInfo info, @PathParam("id") String id,
@QueryParam("fields") List<String> fields,
@QueryParam("expand") List<String> expand) {
Comment comment = Comment.findComment(id);
if (comment == null) {
throw new UnknownResourceException();
}
return new CommentResource(info, comment, fields, expand);
}
@Path("/{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response updateComment(@Context UriInfo info,
@PathParam("id") String id, Map properties) {
Comment comment = Comment.findComment(id);
if (comment == null) {
throw new UnknownResourceException();
}
if(!comment.getAuthor().equals(userService.getCurrentUser())){
throw new OnlyCommentsCreatorCanModifyException();
}
try {
BeanUtils.populate(comment, properties);
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
comment.merge();
return Response.ok(new CommentResource(info, comment),
MediaType.APPLICATION_JSON).build();
}
@Path("/{id}")
@DELETE
public void deleteComment(@PathParam("id") String id){
Comment comment = Comment.findComment(id);
if (comment == null) {
throw new UnknownResourceException();
}
if(!comment.getAuthor().equals(userService.getCurrentUser())){
throw new OnlyCommentsCreatorCanDeleteException();
}
comment.remove();
}
}
Pode-se notar ainda nesses métodos que estamos fazendo uma validação básica de autoria das postagens e comentários para permitir editá-los e deletá-los. Para respondermos ao cliente da API informando os erros nos casos em que essas validações não passarem, criamos novas exceções e devemos mapeá-las no arquivo restErrors.properties.
As exceções OnlyCommentsCreatorCanDeleteException, OnlyCommentsCreatorCanModifyException, OnlyPostsCreatorCanDeleteException e OnlyPostsCreatorCanModifyException são simples exceções que herdam de RuntimeException. A Listagem 15 mostra as novas linhas adicionadas ao restErrors.properties para configurar a resposta dada para essas exceções.
#403
OnlyPostsCreatorCanModifyException = status=403 | code=301 |
msg=Only the creator of a post can modify it. |
devMsg=Only the creator of a post can modify it. | infoUrl=//www.devmedia.com.br/javamagazine/api/errors/301
OnlyPostsCreatorCanDeleteException = status=403 | code=302 |
msg=Only the creator of a post can delete it. |
devMsg=Only the creator of a post can delete it. | infoUrl=//www.devmedia.com.br/javamagazine/api/errors/302
OnlyCommentsCreatorCanModifyException = status=403 | code=303 |
msg=Only the creator of a comment can modify it. |
devMsg=Only the creator of a comment can modify it. | infoUrl=//www.devmedia.com.br/javamagazine/api/errors/303
OnlyCommentsCreatorCanDeleteException = status=403 | code=304 |
msg=Only the creator of a comment can delete it. |
devMsg=Only the creator of a comment can delete it. | infoUrl=//www.devmedia.com.br/javamagazine/api/errors/304
Com isso, concluímos todos os métodos mais básicos da API e já somos capazes de iniciar o servidor para fazermos testes interagindo com os controladores de usuários e postagens. Porém, ainda não somos capazes de criar comentários, pois especificamos que esses seriam criados sob a URL /posts/{id}/comments, o que ainda não foi implementado. Além disso, também não é possível listar os comentários de uma postagem específica ou listar as postagens e comentários de um usuário específico. A seguir, implementaremos essas funcionalidades restantes.
Implementando os demais métodos
Para concluir o que foi especificado, precisamos implementar ações a serem realizadas com base em um recurso selecionado. No nosso caso, tornaremos possível a listagem e criação de comentários com base em uma postagem selecionada, bem como a listagem de postagens e comentários de determinado usuário.
Esse tipo de ação é realizada em URLs do tipo /{recurso1}/{id}/{recurso2}, onde a primeira parte da URL seleciona o recurso raiz através do identificador e a segunda parte especifica um sub recurso do mesmo, o qual receberá alguma ação de acordo com o método HTTP.
A maneira mais natural de se pensar em como implementar algo assim é imaginar que os métodos que tratam a parte da requisição referente ao sub recurso estejam presentes nos próprios recursos retornados pelos controladores.
Dessa forma, o algoritmo seria o seguinte: a primeira parte da requisição é tratada pelo controlador, o qual retorna o recurso selecionado. Daí, procura-se por métodos capazes de tratar a segunda parte da requisição nesse recurso selecionado, chamando o método correto se presente. Esse algoritmo é o adotado pelo JAX-RS e o que usaremos para implementar as funcionalidades restantes.
Existe, no JAX-RS, o conceito de sub-recurso e localizador de sub-recurso. O localizador é o recurso retornado, que passa a ter a função de tratar o restante da requisição, ao invés de ser diretamente retornado como resposta. Nele estão presentes os métodos que localizam os sub-recursos, ou melhor, métodos que tratam o restante da requisição, podendo retornar um sub-recurso no caso de uma requisição de GET.
Um fato importante a se notar é que pode haver uma dualidade entre localizador e recurso. Para isso, o localizador deverá possuir um método que trate a requisição de GET sem especificar um subcaminho, de tal modo que esse método retorne a própria instância do localizador/recurso.
Dessa forma, o localizador localiza a si mesmo para essa requisição, criando a dualidade esperada. Para não ficar tão abstrato, partiremos para a implementação do método que retorna as postagens de um usuário.
O primeiro passo para isso é retirar a anotação @GET do método getUser() do UserController. Assim, a assinatura desse método ficará como na Listagem 16. O motivo disso é que as anotações que identificam os métodos HTTP indicam um ponto final para o JAX-RS. Ao identificar uma delas, o servidor dá como resposta o objeto retornado no método, concluindo a requisição. Ao removermos essa anotação, estamos avisando o servidor que não estamos retornando o resultado final, e sim um localizador de sub-recurso. Note que também podemos remover a anotação @Produces, deixando apenas a @Path.
@Path("/{id}")
public UserResource getUser(@Context UriInfo info, @PathParam("id") String id,
@QueryParam("fields") List<String> fields,
@QueryParam("expand") List<String> expand){
// ...
}
Executando o sistema após essa modificação, notamos que não conseguimos mais selecionar um usuário pelo identificador, pois agora acontece um erro. Isso acontece porque nosso objeto UserResource, que é retornado por esse método, agora é tratado como localizador de sub recurso, porém ainda não possui qualquer método para tratar o restante da requisição.
Portanto, para que a seleção de usuário volte a funcionar, inserimos o método apresentado na Listagem 17 na classe UserResource. Note que esse método não especifica um sub caminho para a requisição, o que o caracteriza como o método usado para criar a dualidade de localizador e recurso para a classe UserResource.
Com essa configuração, a requisição de GET por /users/{id} é tratada pelo controlador, que retorna uma instância de nossa classe, e então tratada por essa instância, que retorna a si mesma como resposta.
@GET
@Produces(MediaType.APPLICATION_JSON)
public UserResource getUserResource(){
return this;
}
Deve-se notar nessa listagem que não especificamos mais nada para esse método, apenas o @GET e o @Produces. Ou seja, não existe qualquer definição de sub caminho nessa requisição. Assim, a requisição de GET por /users/{id} é primeiramente tratada por nosso controlador, que retorna UserResource como localizador.
Com a instância de um localizador, o JAX-RS busca por um método que possa tratar a requisição e encontra o método getUserResource(), que retorna a própria instância de UserResource. Com isso, podemos perceber uma dualidade, em que nossa classe se comporta tanto como localizador quanto como recurso a ser retornado como resposta.
O objetivo disso tudo é criar métodos em nosso localizador, para que possamos selecionar as postagens e os comentários de nosso usuário. Mas para que a seleção básica do próprio usuário continue funcionando, esse tipo de abordagem é necessário.
O mais importante a ser notado aqui é a dualidade de nossa classe, que passou a ser tanto um localizador como um recurso. Isso nos dá flexibilidade para adicionar qualquer método nela, inclusive um que retorne outro localizador.
Para continuarmos com o passo a passo do exemplo, vamos agora implementar o método para obter as postagens criadas por um usuário. Esse método tem a mesma aparência dos métodos presentes no UserController, realizando algo parecido com o método de listagem existente no mesmo. A diferença é que temos acesso aos dados do nosso UserResource, que tem de importante o href, de onde obteremos o identificador do usuário selecionado. A Listagem 18 mostra a implementação desse método.
@GET
@Path(Link.POSTS)
@Produces(MediaType.APPLICATION_JSON)
public CollectionResource listPosts(@Context UriInfo info,
@QueryParam("fields") List<String> fields,
@DefaultValue("false") @QueryParam("expand") boolean expand,
@DefaultValue(CollectionResource.DEFAULT_OFFSET+"")
@QueryParam("offset") int offset,
@DefaultValue(CollectionResource.DEFAULT_LIMIT+"")
@QueryParam("limit") int limit){
String id = getHrefLastPathSegment();
List<Post> posts = Post.findPostsByAuthorId(id, offset, limit);
List<Link> postResources = new ArrayList<Link>(posts.size());
if(expand){
for(Post post : posts){
postResources.add(new PostResource(info, post, fields, null));
}
}
else {
for(Post post : posts){
postResources.add(new Link(info, post));
}
}
return new CollectionResource(info, getPathForPosts(id), postResources);
}
Nessa listagem vemos que a estrutura de um método de um localizador é igual à de um controlador. Podemos perceber a grande semelhança desse com outros métodos de listagem vistos nos controladores criados anteriormente.
Existem apenas duas diferenças notáveis no que foi apresentado na Listagem 18: as chamadas para getHrefLastPathSegment() e getPathForPosts(id).
O primeiro é um método da classe Link que pega o último segmento da URL ajustada no href. Podemos lembrar que a href de um recurso simples é dada por /{recurso}/{id}, o que significa que o último segmento dela será o identificador de nosso recurso.
O outro é um método existente na própria classe UserResource que constrói o caminho no estilo /users/{id}/posts, para que esse seja ajustado como href da coleção de postagens.
Seguindo a mesma estrutura da Listagem 18, podemos implementar a listagem dos comentários do usuário. Essa implementação é semelhante à da listagem das postagens do usuário e também usa um método para construir a href da coleção de comentários. A Listagem 19 mostra a implementação desse método.
@GET
@Path(Link.COMMENTS)
@Produces(MediaType.APPLICATION_JSON)
public CollectionResource listComments(@Context UriInfo info,
@QueryParam("fields") List<String> fields,
@DefaultValue("false") @QueryParam("expand") boolean expand,
@DefaultValue(CollectionResource.DEFAULT_OFFSET+"")
@QueryParam("offset") int offset,
@DefaultValue(CollectionResource.DEFAULT_LIMIT+"")
@QueryParam("limit") int limit){
String id = getHrefLastPathSegment();
List<Comment> comments = Comment.findCommentsByAuthorId(id, offset, limit);
List<Link> commentResources = new ArrayList<Link>(comments.size());
if(expand){
for(Comment comment : comments){
commentResources.add(new CommentResource(info, comment, fields, null));
}
}
else {
for(Comment comment : comments){
commentResources.add(new Link(info, comment));
}
}
return new CollectionResource(info, getPathForComments(id), commentResources);
}
Por fim, resta implementarmos os métodos de criação de comentário e listagem dos comentários de determinada postagem. Para isso, usaremos a mesma estratégia de localizador e sub-recurso, removendo @GET e @Produces do método getPost() e criando três métodos: aquele que retorna o próprio recurso e os outros dois das funcionalidades desejadas. A diferença aqui é que possuímos uma dependência extra necessária no método de criação de comentário: UserService. Esse objeto será utilizado para obter o autor do comentário no momento de sua criação.
Por isso, além de removermos as anotações do getPost(), também criaremos um ponto de injeção da dependência de UserService na classe UserResource. Isso é mostrado na Listagem 20, a qual contém a versão final do método getPost(), o qual agora injeta a dependência de UserService no PostResource criado. Para concluir essa funcionalidade, a Listagem 21 mostra o código a ser adicionado no PostResource, o qual armazena a dependência de UserService, disponibiliza o método usado em sua injeção e cria os métodos do localizador.
@Path("/{id}")
public PostResource getPost(@Context UriInfo info, @PathParam("id") String id,
@QueryParam("fields") List<String> fields,
@QueryParam("expand") List<String> expand) {
Post post = Post.findPost(id);
if (post == null) {
throw new UnknownResourceException();
}
PostResource postResource = new PostResource
(info, post, fields, expand);
postResource.setUserService(userService);
// Injeção da dependência do UserService
return postResource;
}
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
@POST
@Path(Link.COMMENTS)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createComment(@Context UriInfo info, Comment comment) {
comment.setAuthor(userService.getCurrentUser());
comment.setDateCreated(new Date());
comment.setPost(Post.findPost(getHrefLastPathSegment()));
comment.persist();
CommentResource commentResource = new CommentResource(info, comment);
return BaseController.created(commentResource);
}
@GET
@Path(Link.COMMENTS)
@Produces(MediaType.APPLICATION_JSON)
public CollectionResource listComments(@Context UriInfo info,
@QueryParam("fields") List<String> fields,
@DefaultValue("false") @QueryParam("expand") boolean expand,
@DefaultValue(CollectionResource.DEFAULT_OFFSET+"")
@QueryParam("offset") int offset,
@DefaultValue(CollectionResource.DEFAULT_LIMIT+"")
@QueryParam("limit") int limit){
String id = getHrefLastPathSegment();
List<Comment> comments = Comment.findCommentsByPostId(id, offset, limit);
List<Link> commentResources = new ArrayList<Link>(comments.size());
if(expand){
for(Comment comment : comments){
commentResources.add(new CommentResource(info, comment, fields, null));
}
}
else {
for(Comment comment : comments){
commentResources.add(new Link(info, comment));
}
}
return new CollectionResource(info, getPathForComments(id), commentResources);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public PostResource getPostResource(){
return this;
}
No método createComment(), mostrado na Listagem 21, observamos que ele ajusta o campo da data de criação, do autor do comentário e ajusta a postagem a qual o novo comentário pertence. Por fim, ele apenas persiste o novo comentário e responde ao cliente confirmando a criação.
Com isso, concluímos a implementação do que foi planejado para nossa API. No entanto, vale lembrar que ainda há muito o que melhorar nela. Mas antes de comentarmos o que o leitor poderia fazer para tornar essa API melhor, vamos realizar alguns testes.
Testando a API
Para testarmos a API, devemos fazer requisições de criação e obtenção dos recursos. Para facilitar essa tarefa, podemos utilizar um cliente REST, o qual permite configurar e executar requisições, demonstrando o resultado das mesmas. Existem muitos clientes disponíveis e algumas IDEs até incluem um cliente REST, mas uma opção simples que usaremos aqui é a instalação de uma aplicação do Chrome.
Aqui usaremos o Postman – REST Client, cuja URL de instalação é indicada na seção Links. Sua utilização é bem simples e suas funções são bem completas. Ele torna possível a realização de requisições complexas em que se especifica o método HTTP, a URL e os diversos tipos de parâmetros.
Podemos começar os testes com esse cliente inserindo uma postagem através da API. Para isso, iremos inserir a URL adequada (localhost:8080/restfulapi/api/posts), selecionar o método de POST, selecionar a opção “raw” para que possamos especificar livremente o corpo da requisição e inserir o JSON apresentado na Listagem 2 no campo do corpo.
Isso tudo é mostrado na Figura 2. Devido à nossa implementação, essa requisição criará uma postagem e atribuirá o usuário padrão simulado pelo UserService como autor da mesma.
Para testarmos a paginação, podemos inserir várias postagens da mesma forma, mudando apenas o campo de título e texto para podermos diferenciá-las. Por fim, podemos mudar o tipo de requisição para GET e veremos o resultado da coleção de postagens de maneira paginada.
Se não incluirmos qualquer parâmetro, veremos apenas os links para as postagens. Para ver a representação padrão delas, devemos clicar em “URL Params” e incluir o parâmetro “expand” com valor true. Ainda podemos especificar os campos desejados através da adição do parâmetro “field” múltiplas vezes, cada uma com o nome de um dos campos desejados. Outros parâmetros a serem adicionados são o de “offset” e “limit”, para testarmos a paginação dos resultados.
A Figura 3 mostra a configuração desse tipo de requisição.
Esse mesmo teste de criação e listagem pode ser feito com o recurso de usuário. Ao solicitarmos a listagem de usuários pela primeira vez, verificamos que já existe um usuário que foi criado para ajustar o autor de nossa postagem. Podemos criar usuários da mesma forma que fizemos com as postagens, mas usamos o JSON da Listagem 1 como base.
Com postagens e usuários criados, podemos ainda listar as postagens de nosso usuário padrão. Para isso, fazemos uma requisição pela listagem de usuários e procuramos por ele no resultado. Em seguida, copiamos seu identificador e fazemos uma requisição do tipo GET para a URL localhost:8080/restfulapi/api/users/{id}/posts.
Para testar a parte de comentários, devemos criar comentários em uma postagem específica. Para tanto, pode-se pegar o identificador de uma postagem criada anteriormente e fazer uma requisição de POST para a URL localhost:8080/restfulapi/api/posts/{id}/comments, incluindo o JSON da Listagem 3 como corpo da requisição. Com múltiplos comentários criados dessa forma, podemos requisitar pela listagem dos comentários de tal postagem com uma simples mudança no tipo da requisição para GET no lugar do POST.
Devemos testar ainda a edição e deleção de nossos recursos. Para isso, devemos fazer requisições de PUT e DELETE para as URLs encontradas como href dos mesmos. No caso do DELETE, nenhum parâmetro extra precisa ser adicionado. No caso do PUT, devemos colocar um JSON como corpo da requisição, o qual deve incluir os campos a serem editados.
Assim, concluímos os testes de nossa API. Nesses testes, podemos notar que obtemos uma boa navegação pela API através dos links que ela mesma nos apresenta nos resultados. Isso significa que estamos utilizando bem o conceito de HATEOAS, o que é essencial para uma API REST.
Comentários finais e melhorias na API
Até aqui, produzimos uma API simples, porém utilizamos bastante a principal boa prática do REST: HATEOAS. A API criada possui boa navegabilidade através dos links incluídos na mesma, tornando-a muito intuitiva para nossos clientes. Além disso, seguimos muito bem todos os padrões do REST, bem como suas boas práticas.
Apesar disso, ainda há muito que melhorar nessa API para torná-la boa o suficiente para entrar em produção. A partir de agora, falaremos de alguns pontos que não serão implementados neste artigo, mas que recomendamos fortemente que o leitor os conclua.
Adição de mais links que facilitem a paginação
Quando projetávamos a listagem de recursos, falamos bastante sobre os hyperlinks de cada recurso e também de alguns adicionais que poderão ser incluídos. São eles: hyperlink para a próxima página, para a página anterior, para a primeira e para a última página.
Esses hyperlinks podem aparecer no resultado final da requisição de acordo com a disponibilidade. Por exemplo, se foram requisitados 25 recursos e existem apenas 15, não há necessidade de incluirmos o hyperlink da próxima página.
Essas adições deixariam nossa API ainda mais natural e navegável, tornando possível até mesmo a criação de clientes mais inteligentes, que utilizem esses links para paginar, ao invés de calcular cada um desses hyperlinks para realizar a paginação. Com isso, tornaríamos ainda mais forte o uso de HATEOAS em nossa API com uma simples implementação na classe ResourceCollection.
Adição de filtragem nos métodos de listagem
Para possibilitar uma busca por recursos no estilo REST, podemos permitir a filtragem dos resultados nos próprios métodos de listagem. Para isso, devemos incluir um parâmetro extra e fazer a filtragem dos resultados com base nesse parâmetro. Assim, os clientes podem usar a mesma URL para listar todos os recursos e também para fazer uma busca neles.
Evitar a exposição de nossas entidades
Quando criamos o método de criação de usuários, foi comentado sobre o uso da entidade User como parâmetro do método. Como informado anteriormente, isso está errado e deve ser evitado. Portanto, uma melhoria necessária para nossa API é a substituição desse parâmetro por um Map, de onde obteríamos os campos de nosso usuário.
Além disso, também expomos nossas entidades quando utilizamos o método utilitário de BeanUtils para preenchê-las automaticamente com os campos vindos de um Map.
Isso porque esse utilitário só faz seu trabalho se o mapa e a entidade possuírem campos de mesmo nome. Assim, perdemos a flexibilidade de mudarmos os nomes dos campos de nossas entidades sem interferir em nossa API.
Portanto, para evitar tal exposição de nossas entidades, devemos preencher seus campos manualmente, evitando o uso de utilitários que façam isso automaticamente.
Um passo adicional seria a criação de validações na API, criando exceções e as adicionando no arquivo de configuração de exceções. Isso porque atualmente a API está fazendo tais validações através da especificação Bean Validation.
Essa especificação considera as anotações feitas em atributos da entidade, como @NotNull, e sua implementação (Hibernate Validation, por exemplo) retorna uma exceção genérica quando acontece um erro de validação.
Assim, além de tornar necessária a exposição de nossa entidade, não há uma forma natural de converter esses erros para o formato desejado em nossa API, que inclui os campos que descrevem o erro na resposta da requisição.
Portanto, uma possível melhoria seria a criação de validações na própria API, onde também criaríamos novas exceções e as mapearíamos através do arquivo restErrors.properties.
Versionamento
Muito foi falado sobre versionamento na primeira parte deste artigo, mas ela não foi implementada aqui. Com a implementação feita, a qual separa os recursos de suas respectivas entidades (UserResource, PostResource e CommentResource), em conjunto com as modificações sugeridas até aqui, chegamos a um ponto em que eles estão bem desacoplados de nossas entidades. Isso facilita bastante a criação de um versionamento de nossos recursos. Para isso, poderíamos criar classes novas para cada versão de determinado recurso. Poderíamos, por exemplo, versionar o recurso de usuário através de classes no estilo UserResourceV1, UserResourceV2 e assim por diante. Deste modo, somos capazes de selecionar a versão correta de acordo com o especificado na requisição.
Uma possível modificação em um recurso que necessitaria de versionamento seria a divisão do campo de nome do usuário em dois novos campos: primeiro e segundo nome. Para isso, criaríamos uma nova versão do recurso de usuário, que passaria a exibir esses dois campos no lugar do campo original de nome.
Porém, para mantermos a compatibilidade e não quebrarmos os softwares de nossos clientes, podemos manter a versão antiga, em que o campo de nome do usuário é construído através da concatenação dos novos campos de primeiro e último nome.
No momento do cadastro, poderíamos dividir a String passada e separá-la em primeiro e segundo nome. Assim, seria mantida a funcionalidade normal dos clientes que não atualizaram ainda para a nova versão.
Adição de mais localizadores
Ainda que não seja tão necessário, seria bom ter uma API que seja flexível o suficiente a ponto de permitir requisições do tipo GET para URLs como /users/{idUsuario}/posts/{idPostagem}. Para permitir esse tipo de requisição, que poderia possuir ainda mais sub-recursos envolvidos, precisamos transformar mais recursos em localizadores.
Na verdade, precisamos tornar uma única classe como localizadora: CollectionResource, visto que ela é a última classe utilizada como recurso e retornada como resposta que ainda não se comporta também como localizadora.
Para isso, removeríamos todas as anotações @GET dos métodos de listagem, adicionaríamos um método em CollectionResource com @GET que retornaria a própria instância de CollectionResource e então criaríamos um método que recebe um identificador como parâmetro e busca pelo recurso na coleção de itens presente nessa classe.
Para tornar esse processo mais fácil, podemos adicionar um campo privado com o identificador do recurso na classe de onde todos os recursos herdam: Link.
Visto que ela continuaria sendo um Map e que o serializador de JSON considera apenas as propriedades do mapa, essa propriedade não seria incluída no JSON final e poderia ser usada apenas internamente na API.
Assim também poderíamos remover o método que obtém o identificador através da href e utilizar diretamente o identificador do recurso.
Segurança
Por fim, precisamos tornar nossa API segura, criando uma real autenticação de nossos usuários, ao invés de simular sempre um usuário padrão. Para isso, recomenda-se o estudo da biblioteca de OAuth fornecida pelo Apache CXF, usando-a em conjunto com alguma API de controle de segurança, como Spring Security ou Apache Shiro.
Como foi dito na primeira parte deste artigo, OAuth é a especificação de segurança criada para REST, sendo usada na maioria das APIs mais conhecidas. Apesar do amplo uso, é uma especificação muito difícil de entender por completo e de integrá-lo ao sistema.
Ao pesquisarmos, verificamos poucas bibliotecas que auxiliam nessa tarefa. Por esse motivo foi escolhido o Apache CXF como implementação do JAX-RS, no lugar do famoso Jersey. Esse framework da Apache inclui um módulo para implementação da especificação OAuth1.0a e OAuth2, o que torna esse processo muito mais fácil.
Com isso, nesta segunda parte do artigo, colocamos em prática os conceitos e boas práticas de REST que foram apresentados na primeira parte. Através deles, implementamos uma API completa e intuitiva, que segue todas as diretrizes definidas pelo REST.
Depois de terminarmos uma versão básica da API, analisamos o que podemos fazer para torná-la uma API real. Neste momento foi recomendado ao leitor que siga as instruções finais para concluir a implementação da API. E como um complemento importante, para auxiliar o desenvolvimento da parte de autenticação, foi indicada a leitura da especificação do OAuth e da documentação do Apache CXF.
Por fim, com a conclusão desta série, acredita-se que o leitor tenha tido uma experiência suficiente com web services no estilo REST para que venha a implementá-los seguindo os padrões e melhores práticas. Devemos lembrar que esse tipo de web service é aquele que apresenta melhor aceitação em clientes leves, como smartphones e tablets, dispositivos cujo mercado está em pleno crescimento. Isso demonstra a importância do REST e porque devemos implementar web services desse tipo.