Como avaliar uma API

Como avaliar uma API
HTTP REST

Esse post era para ter sido um capítulo do meu livro Roadmap back-end: Conhecendo o protocolo HTTP e arquiteturas REST mas acabou sendo descartado na edição final. Por isso decidi publica ele no blog sem revisão, ou seja, pode existir erros de ortografia.

Nos capítulos anteriores vimos as especificações do que é o protocolo HTTP e do que são APIs REST. Agora vamos caminhar tentando trazer mais elementos a nossas APIs. Para tentar responder a pergunta como podemos avaliar uma API podemos levantar algumas possíbilidades, desde a identificação de padrões e antipadroes, análise da complexidade, até por fim chegarmos a maturidade. Então nosso primeiro desafio será responder à seguinte pergunta: existe alguma definição de padrões de projetos para APIs REST?

Antes de responder a essa pergunta precisamos definir o que é um padrão de projeto, o que é um antipadrão e como eles podem nos ajudar.

Padrões e antipadrões de projetos

A primeira pergunta que vamos responder é: de onde vem o conceito de padrões de projeto?

Padrões não surgiram na computação, é um conceito importado da arquitetutra. Quando se começa a pensar em uma casa, é preciso fazer inúmeras escolhas sobre pequenas coisas para resolver problemas triviais. Essas escolhas já envolvem alguns padrões que são universais como “porta”, “chão”, “mesa” e outros móveis. Para catalogar outros padrões, Christopher Alexander escreveu o livro House generated by patterns em 1969 em que ele descreve várias estruturas arquitetônicas e urbanísticas usando uma estrutura descritiva bem simples mostrando o contexto, a solução e uma análise dos problemas que a solução pode trazer. Esse livro deu origem a outros projetos, incluindo o livro A Pattern Language: Towns, Buildings, Construction de 1977, em que Alexander cria uma linguagem usando padrões e o conceito de Linguagem de Padrões. Descrição de projetos são simplificadas usando uma linguagem de padrões, pois ao invés de descrever detalhadamente o projeto é possível referenciar os padrões já descritos, cada padrão vai apresentar a melhor solução até o momento para um determinado problema.

Padrão Gradiente de Intimidade descrito no livro "House generated by patterns"

Em 1987, Kent Beck e Ward Cunningham escreveram o artigo Using Pattern Languages for Object-Oriented Programs aplicando o conceito de Linguagem de Padrões para interfaces gráficas de programas Smalltalk. No artigo apenas um padrão é descrito textualmente, o “Collect Low-level Protocol”, mas os autores relatam que estavam descrevendo uma linguagem com mais de 150 padrões.

Em 1994, Doug Lea publicou um artigo na revista Software Engineering Notes chamado Christopher Alexander: an introduction for object-oriented designers, onde havia a adaptação dos modelos de padrões urbanisticos de Christopher Alexander para o mundo do software. Nesse artigo vemos pela primeira vez a proposição de um formato para descrição de padrões. Um padrão deveria ter um nome, um exemplo e uma descrição do modelo onde deveria ser usado. Na verdade, no artigo estão presentes outras propriedades que em muitos casos não são facilmente aplicadas, como encapsulamento, generatividade, equilibrio, abstração, abertura e composibilidade.

Com o artigo de Doug Lea, ainda em 1994, Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides se juntam para escrever o livro mais conhecido sobre padrões Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos. Nesse livro podemos encontrar a definição de vários padrões encontrados em projetos de software orientado a objetos. Por ter sido escrito por quatro autores, é popularmente conhecido como Gang of Four (gangue de quatro em tradução livre), ou pela sigla GoF. Os padrões do GoF são apresentados seguindo um modelo mais simples do que o proposto por Lea, cada padrão tem intenção, motivação, aplicabilidade, estrutura, participantes, colaborações, consequências, implementação, exemplo de código, usos conhecidos e padrões relacionados. Os padrões por sua vez são divididos entre padrões de criação, estruturais e comportamentais.

— E como esses padrões surgem?

Ao contrário do que se imagina, os padrões não são propostos, eles são identificados. Padrões já existem em projetos como soluções para problemas recorrentes, o que ocorre é que eles são identificados, especificados e analisados. Existem até conferências para apresentações de padrões, como é o caso da Pattern Language of Programs, em que padrões são lidos e discutidos em grupos.

— E para que servem esses tais padrões? Nunca precisei de um!

Conhecer padrões serve para aumentar o nosso vocabulário como desenvolvedores. Padrões, quando descritos, já vêm acompanhados de uma prévia discussão, mas depois que são propostos eles estão sujeitos a crítica. Alguns padrões recebem tanta crítica que são catalogados como antipadrões. Mas padrões e antipadrões não significam que são de uso imperativo ou não. Eles apenas servem para avaliarmos se uma solução é boa ou não. Um padrão tem sempre um contexto, um problema e uma solução, mas também possui as análises de vantagens e desvantagens que sua aplicação traz.

Quando tempos um bom repertório de padrões, começamos a responder mais rapidamente ao desafio de escrever e revisar código. Podemos identificar estruturas para podermos resolver os problemas de código do dia a dia. E também podemos identificar vantangens e desvantagens de estruturas propostas.

Quem anda escrevendo sobre padrões REST

Antes de escrever sobre padrões REST, precisamos fazer uma busca para identificar e reconhecer quem já escreve sobre o assunto, para identificar algumas publicações interessantes. Nossa discussão vai se limitar aos três artigos a seguir, que podem trazer padrões e ideias interessantes sobre como avaliar uma API REST, embora todos em inglês.

O primeiro artigo propõe uma heurística para determinação de padrões e antipadrões em APIs REST. Como ele foi escrito antes da proposição do OpenAPI, em 2015, sua análise não se baseia em documentações, mas em implementações. Sua análise propõe alguns padrões e antipadrões que mostraremos a seguir, assim como um algoritmo para identificar o padrão dentro do projeto de código.

No segundo artigo Haupt propõe uma análise estrutural baseada na documentação da API usando OpenAPI. As documentações de APIs abertas foram analisadas por uma ferramenta para se extrair o modelo da API. Segundo Haupt, cada API constiria de uma série de recursos possuem métodos e relacionamentos. Ele por sua vez se baseia no Atomic Resource Model e no URI Model. O primeiro descreve uma API através de seus elementos básicos como recursos, verbos e representações. Já o segundo modelo estende o primeiro, dando a oportunidade ao cliente da API de navegar nos recursos baseados em hyperlinks providos pela propria API. Com essa análise é possivel conhecer a complexidade da API, descrevendo quantos recursos ela possui, quantos desses são ReadOnly e quais as relações entre eles através de links.

Metamodelo de uma API

E por fim, no terceiro artigo, Svensson faz uma análise de algumas APIs de mercado para IoT e propões 8 padrões de projetos, baseado no modelo no GoF. APIs de IoT são altamente voltada a recursos, pois cada dispositivo é um recurso. Segundo Svensson, os padrões presentes na literatura são mais voltados para garantir a qualidade RESTful das APIs e não para o desenho dos endpoints, por isso todos esses padrões se concentram na URI.

Revisando a anatomia de uma requisição

Para detalhar as padrões de API REST, precisamos revisar como podemos subdividir uma URI. Segundo a IETF RFC 3986, temos os seguintes componentes: Scheme://Authority/Path?Query#Fragment. Cada componente de uma URI tem sua especificidade e significância que devem ser levadas em conta na construção de uma API, nós vimos no capítulo 3 como cada elemento é descrito, agora vamos expandir essas definições.

O caminho (Path) pode ser subdividdo em base (Base) e principal (Main) Scheme://Authority/Base/Main?Query. A base é identificada quando existe caminhos comuns em uma API, ela pode ter uma função primordial quando precisamos definir qual API e/ou qual versão da API deve ser acesada. Ao se identificar a base, o principal é o ponto crucial da requisição.

Ainda no caminho, cada token pode ser considerado um nó, assim podemos assumir funções para cada nó de uma requisição. Na tabela a seguir temos as várias funções que um nó pode assumir.

Nome Descrição
Nó de acesso Um nó que é usado apenas para direcionar as seguintes partes do URI para uma determinada seção da API, ou seja, não é um recurso ou dados que podem ser buscados.
Nó pai Um nó que geralmente representa uma categoria, um recurso que inclui recursos ou um objeto. Na API e no URI, esse nó existe em uma hierarquia. Aqui, tendemos a mencionar apenas o fim da hierarquia.
Nó indicativo Um nó que representa uma ordem, consulta ou ação a ser realizada em um determinado recurso para aplicar esta ação ou conhecer uma informação específica sobre esse recurso, por exemplo, filtrar os resultados de uma solicitação feita em um determinado recurso. Podemos pensar nisso como um ponto final que dispara uma função. Geralmente é uma palavra convencional, como info, create (criar), first (primeiro), last (ultimo), status etc. Nós indicativos podem ser divididos em três categorias: nós de ação, nós de filtro e nós informacionais
Nós de ação Um nó usado para acionar uma função específica ou aplicar funcionalidades clássicas de CRUD em um recurso, usando qualquer método de solicitação HTTP. Na maioria dos casos, esses nós assumem a forma de um pedido para realizar uma ação, por exemplo, create, clone, upload ou consume
Nós de filtro Um nó usado para direcionar um grupo específico ou estado dos recursos solicitados.
Nós informacionais Um nó usado para obter informações sobre metadados para um ou vários recursos. Esses metadados não podem ser modificados diretamente nem acessíveis por meio do recurso

A classificação da autoridade também pode ser estendida, por exemplo, Svensson a classifica como dinâmica e estática.

CRUD

Todo recurso aceita um conjunto de operações. Quando usamos o termo CRUD estamos nos referindo ao conjunto mais comum de operações Create, Read, Update e Delete (criar, ler, apagar e remover). Essa sigla é muito associada a interfaces gráficas, mas ela também pode ser aplicada a uma API. Apesar de existirem essas quatro operações básicas, um recurso pode ter outras operações como habilitar/desabilitar, associar/desassociar etc. Operações dependem da lógica de negócios, por isso cada aplicação deve definir o conjunto de operações que seu recurso aceita.

Encontrando os padrões

Na literatura citada anteriormente, vamos listar alguns padrões. Eles serão detalhados usando um modelo mais simples que o proposto pelo GoF, cada padrão o seu nome, propositor, forças, vantagens e desvantagens. Cada padrão terá uma breve descrição com marcações em negrito, elas se referem a termos chaves da descrição. Estes são os padrões que vamos apresentar:

Cada padrão será descrito usando o exemplo da API para bibliotecas que criamos no capítulo passado.

Vinculação de entidades

Esse padrão permite a comunicação em tempo de execução através de links providos pelo servidor no corpo da resposta ou via Location:, no cabeçalho da resposta. Usando hyperlinks, a dependência entre cliente e servidor é reduzida permitindo ao cliente automaticamente encontrar as entidades relacionadas em tempo de execução.

Forças

Proposta

Foi proposta em “Detection of REST Patterns and Antipatterns: A Heuristics-Based Approach” por Francis Palma, Johann Dubois, Naouel Moha, e Yann-Gaël Guéhéneuc.

Problema

As ações a que uma entidade pode ser submetida devem ser independentes do cliente. A API deve fornecer informações suficientes para que o cliente consiga compreender as ações que pode realizar com uma entidade.

As entidades relacionadas a uma determinada entidade também devem ser independentes do cliente. Se as relações entre entidades não forem fornecidas pela API, cada cliente deve implementar as relações, acarretando em código duplicado, acoplamento de versões e propagação de bugs.

Solução

Ao acessar ou criar uma entidade, o servidor pode enviar o URL da mesma, de todas entidades relacionadas e possíveis ações dentro do corpo da resposta ou através dos cabeçalhos Location ou Link. A RFC 5988 - Web Linking trata do caso de se usar o cabeçalho Link, no caso há um proposição de formato que pode ser usado também no cabeçalho Location.

POST /tiquete HTTP 1.1
Host: tiquetes.com.br
Content-Type: application/json

{
    "titulo": "Erro ao criar Tíquete",
    "descricao": "A API de criação de tíquetes retorna erro 500 quando nenhuma tag é associada.",
    "projeto": "PRJ-001",
    "epico": "CRIAR-TIQUETE",
    "tags": ["Java", "REST", "CRUD"]
}

201 Created
Location: tiquetes.com.br/tiquete/6942
Link: <Autor>;rel="/tiquete/autor/215";title="Victor Osório",<Projeto>;rel="/projeto/proj-001";title="Tíquetes.COM.BR",<TAG>;rel="/tag/java";title="Java",<TAG>;rel="/tag/rest";title="REST",<TAG>;rel=/tag/crud;title="CRUD";<EPICO>;rel=/epico/criar-tiquete;title="Criar Tíquete"

Uma outra possível apresentação para esse padrão é inseri-lo dentro do corpo da mensagem. Esse padrão é proposto pelo HATEOAS (que significa Hypermedia as the Engine of Application State) e pode ser aplicado inserindo o campo link ao objeto retornado.

{
    "id": 6942,
    "titulo": "Erro ao criar Tíquete",
    "descricao": "A API de criação de tíquetes retorna erro 500 quando nenhuma tag é associada.",
    "projeto": "PRJ-001",
    "epico": "CRIAR-TIQUETE",
    "tags": ["Java", "REST", "CRUD"],
    "links": [
        {
            "href": "/tiquete/6942",
            "rel": "self",
            "type": "GET"
        }, {
            "href": "/tiquete/6942/responsavel",
            "rel": "responsavel_associar",
            "type": "POST"
        }
    ]
}

Cache de resposta

O cacheamento da resposta é uma boa prática para evitar enviar requisições duplicadas e respostas através do cacheamento de todas as mensagens no local da máquina do cliente. São usados os cabeçalhos Cache-Control e ETag, assim como o código HTTP 304.

Forças

Proposta

Foi proposta em “Detection of REST Patterns and Antipatterns: A Heuristics-Based Approach” por Francis Palma, Johann Dubois, Naouel Moha, e Yann-Gaël Guéhéneuc.

Problema

APIs com elevada demanda de requisições devem encontrar meios de reduzir a quantidade de processamento e memória utilizada. Dessa forma recursos podem ser acessados por vários clientes ao mesmo tempo, sem que seja necessário o processamento completo da requisição no servidor, mas cada cliente deve ter sempre a versão mais atualizada do recurso desejado.

Solução

Essa solução já foi demonstrada no capítulo Discutindo o protocolo, onde vimos que o correto uso do cache pode evitar tanto uma sobrecarga do servidor quanto erros de concorrência ao se alterar um recurso. Em ambos os casos se usa o cabeçalho ETag.

Para evitar a sobrecarga do servidor, toda entidade passível de cache deve vir com um valor de ETag. Este valor pode ser um número sequencial ou o hash do conteúdo da entidade. Assim quando for necessário acessar o conteúdo novamente o servidor saberá que o cliente já tem o conteúdo dessa requisição e se este está atualizado ou não.

GET /tiquete/6942 HTTP 1.1
ETag: "8199bab3962576d495a6d2a4ac48abfa"


304 Not Modified

Para evitar problemas de concorrência, uma operação pode ser feita usando o cabeçalho If-Match, assim a operação só poderá ser realizada se não houve nenhuma alteração no estado da entidade.

PATCH /tiquete/6942 HTTP 1.1
If-Match: "8199bab3962576d495a6d2a4ac48abfa"
Content-Type: application/json

{
    "projeto": "PRJ-002"
}

HTTP/1.1 412 Precondition Failed
Date: Sat, 27 Feb 2021 16:12:02 GMT

Negociação de Conteúdo

Esse padrão suporta representações alternativas para recursos (por exemplo, em json, xml, pdf etc.) assim. o serviço consumidor se torna mais flexível com alta reutilização. Servidores podem prover recursos em qualquer formato padrão requerido pelos clientes. Esse padrão é aplicado através dp HTTP Media Types e permite aos usuários da API terem mais liberdade de implementação.

Forças

Proposta

Foi proposta em “Detection of REST Patterns and Antipatterns: A Heuristics-Based Approach” por Francis Palma, Johann Dubois, Naouel Moha, e Yann-Gaël Guéhéneuc.

Problema

Alguns clientes têm limitações de biblioteca de serialização. Em certos clientes há somente a opção de JSON e em outros somente XML, em outro caso, o cliente vai requerer a exportação de certas entidades em vários formatos PDF, XLS ou qualquer outro formado.

Solução

Essa solução já foi demonstrada no capítulo Discutindo o protocolo. Ao cliente é possível escolher qual o formato de mídia que este deseja receber, basta usar o cabeçalho Content. Essa solução pode ser feita tanto para troca de mensagens como para o download de recursos. No primeiro caso se aplica quando o cliente tem alguma limitação de serialização e o segundo é quando o cliente deseja um formato especifico. No caso a seguir vamos mostrar como fazer o download em formato EPUB, o formato poderia ser escolhido pelo cliente.

GET /tiquete/6942 HTTP 1.1
Content: application/pdf

200 OK
[Conteúdo em format PDF]

Redirecionamento de end-point

A funcionalidade de redirecionamento através da web é suportado por este padrão, que também desempenha um papel importante como meio de composição de serviços. Para redirecionar clientes, o servidor envia uma nova localidade para acompanhar um dos códigos HTTP entre 301, 302, 307 ou 308. O principal benefício desse padrão é que um serviço alternativo continua ativo mesmo que o endpoint requerido não responda.

Forças

Proposta

Foi proposta em “Detection of REST Patterns and Antipatterns: A Heuristics-Based Approach” por Francis Palma, Johann Dubois, Naouel Moha, e Yann-Gaël Guéhéneuc.

Problema

Nem sempre identificadores são imutáveis, em alguns casos eles podem ser alterados. Entidades podem ser localizada a partir de campos alteráveis, ou seja, sempre que um identificador for alterado o identificador antigo deve redirecionar para o novo. Em outros casos a API pode mudar a sua topologia, criando novos formatos de URI, mas mesmo assim ele pode responder a requisições usando a topologia antiga.

Solução

A solução é usar o código de estato 301 associado ao cabeçalho Location, conforme definido no protocolo HTTP. Para exemplificar, vamos mostrar o caso mais comum, quando um recurso muda de identificador.

GET /epico/criar-tiquete HTTP 1.1

301 Moved Permanently
Location: /epico/crud-tiquete

End-point da Entidade

Serviços com um único endpoint são pouco granulares. Usualmente, um cliente requer ao menos dois identificadores: um global para o serviço em si e um local para o recurso ou entidade gerenciada pelo serviço. Aplicando esse padrão, isto é, usando multiplos endpoints, cada entidade (ou recurso) de um serviço incorporado pode ter seu identicador único e endereço global.

Forças

Proposta

Foi proposta em “Detection of REST Patterns and Antipatterns: A Heuristics-Based Approach” por Francis Palma, Johann Dubois, Naouel Moha, e Yann-Gaël Guéhéneuc.

Problema

Recursos que compartilham end-point tornam o cache impraticável, além de inserir complexidade desnecessária a uma API. Cada recurso deve ter seu end-point único, assim ele pode ser acessado diretamente.

Solução

Cada entidade deverá ter um end-point associado para retornar as informações pode identificador. Assim /tiquete/:id: vai retornar as informações do tíquete conforme seu identificador, a mesma regra deve valer para sprint (/sprint/:id:), épico (/epico/:id:) e assim por diante.

GET /tiquete/6942 HTTP 1.1

{
    "id": 6942,
    "titulo": "Erro ao criar Tíquete",
    "descricao": "A API de criação de tíquetes retorna erro 500 quando nenhuma tag é associada.",
    "projeto": "PRJ-001",
    "epico": "CRIAR-TIQUETE",
    "tags": ["Java", "REST", "CRUD"]
}

URI direcionado antecipadamente

Para direcionar a requisição da URI para um grupo específico e único dentro da API. Pode ser um ID de organização, código de área ou nome de servidor. Normalmente é usado um nome variável no começo da Autoridade.

Forças

Proposta

Foi proposta em “Defining Design Patterns for IoT APIs” por Rasmus Svensson, Adell Tatrous e Francis Palma.

Problema

Algumas APIs requerem agrupamentos lógicos que vão impactar não somente a distribuição dos recursos, mas toda a lógica da API. Requisições direcionadas a esses agrupamentos lógicos vão acarretar em estatisticas separadas, assim como limitações de quotas ou controle de acesso. Existe uma entidade raiz onde todas as outras entidades que serão cadastradas pertencem a essa entidade raiz não podendo ser compartilhadas com as outras entidades raiz.

Solução

Para cada entidade raiz, deve ser criada uma nova autoridade. Vamos supor que podemos cadastrar um produto nova. Cada produto terá seu identificador que deve ser usado na URL para acessar a API referente a ele.

POST /produto HTTP 1.1
Host: tiquetes.com.br
Content-Type: application/json

{
    "id": "biblioteca",
    "nome": "Gestão de emprestimo de livros",
    "descrição": "API para gestão de emprestimo de livros. Deverá ser usado por várias bibliotecas"
}

201 Created
Location: https://biblioteca.tiquetes.com.br

Requisição Expressa

Para executar as funcionalidades clássicas do CRUD ou executar uma função específica em um recurso, ao mesmo tempo em que declara claramente a finalidade do URI e não apenas se baseia no método usado através de um Nó de Ação na seção Main.

Forças

Proposta

Foi proposta em “Defining Design Patterns for IoT APIs” por Rasmus Svensson, Adell Tatrous e Francis Palma.

Problema

A URI deve declarar expressamente qual operação está sendo realizada. Operações de remoção devem ser ter informações associadas a operação. Certas entidades são passíveis de vários tipos de ações: checkout, reserva, compra, venda etc.

Solução

Para expressar qual operação está sendo feita, cada operação deve ter um identificador e este deve ser usado como um nó de ação do final do end-point da entidade.

PUT /tiquete/6942/assumir HTTP 1.1

{
    "usuario": 732
}

200 OK

‘me’ Recurso Acessível

Para apontar para o usuário autenticado no momento ao solicitar recursos ou executar ações às quais este usuário tem acesso, use um nó me no início da seção Main.

Forças

Proposta

Foi proposta em “Defining Design Patterns for IoT APIs” por Rasmus Svensson, Adell Tatrous e Francis Palma.

Problema

Certas regras de negócios podem requerer end-points personalizados. Esses end-points vão retornar recursos especificos e o end-point pode encapsular regras de negócios complexas como consultas.

Solução

O caso mais comum desse padrão é o end-point /me que retorna os dados do usuário autenticado. Para nosso sistema, podemos criar o end-point /tiquete/meus que retorna todos os tíquetes com que o usuário logado está relacionado. Esse end-point pode ainda ter um filtro /tiquete/meus/ativos que retorna apenas os tiquetes ativos da consulta anterior.

GET /me HTTP 1.1

{
    "id": 319,
    "nome": "Victor Osório",
    "usuario": "vepo"
}

Acessibilidade dos Metadados

Para ler informações, principalmente usando o método GET, sobre metadados para um único ou vários recursos, como: count, state, status ou outros dados que não podem ser modificados diretamente nem acessíveis por meio de um recurso, o URI pode ter um nó informativo significativo no final da seção Main como uma indicação para as informações solicitadas.

Forças

Proposta

Foi proposta em “Defining Design Patterns for IoT APIs” por Rasmus Svensson, Adell Tatrous e Francis Palma.

Problema

Metadados são entidades que são controladas pela API, o cliente pode apenas consultar os metadados. A maioria das necessidades de negócio não dependem de metadados, logo eles não devem ser retornados juntamente ao recurso.

Solução

Os metadados são expostos por um end-point adicionando um nó informacional ao end-point do recurso. Assim as estatísticas de um projeto podem ser lidos através de /projeto/:id:/estatisticas.

GET /projeto/proj-001/estatisticas HTTP 1.1

{
    "colaboradores": 12,
    "horasTrabalhadas": 612,
    "mediaTimeToMarketEmDias": 12
}

Filtragem Proativa

Para direcionar um grupo específico ou estado do recurso solicitado sem depender de um parâmetro de consulta dedicado, use um nó de filtro na seção Main.

Forças

Proposta

Foi proposta em “Defining Design Patterns for IoT APIs” por Rasmus Svensson, Adell Tatrous e Francis Palma.

Problema

Todo sistema possui um conjunto de consultas que são executadas diversas vezes, mas cuja lógica é complicada. Para esses casos, devem existir filtros padrões na API, as regras de negócios devem ser encapsuladas pela API, sendo acessados diretamente.

Solução

Esses filtros podem ser expressos como nós informacionais adicionados a end-point de recursos. Por exemplo, para se requerer todos os tíquetes pendentes para o sprint que o usuário logado está participando /sprint/atual/pendentes, esse mesmo padrão pode ser usado para qualquer sprint /sprint/:id:/pendentes.

GET /sprint/atual/pendentes HTTP 1.1


[{
    "id": 6942,
    "titulo": "Erro ao criar Tíquete",
    "descricao": "A API de criação de tíquetes retorna erro 500 quando nenhuma tag é associada.",
    "projeto": "PRJ-001",
    "epico": "CRIAR-TIQUETE",
    "tags": ["Java", "REST", "CRUD"]
}, {
    "id": 6948,
    "titulo": "Associar Tíquete a Sprint",
    "descricao": "Criar endpoint para associar tíquete a um sprint especifico",
    "projeto": "PRJ-001",
    "epico": "SPRINT",
    "tags": ["Java", "REST"]
}, {
    "id": 6953,
    "titulo": "Tela de consulta de Tíquetes",
    "descricao": "Criar tela de consulta de tíquetes, deve ser possível consultar tiquete por todos os campos",
    "projeto": "PRJ-001",
    "epico": "FRONTEND",
    "tags": ["Javascript", "Frontend"]
}]

API versionada

A diferenciação de versão da API é feita na request. Esta pode ser feita com base em um nó no Base, que representa a versão. Essa diferenciação também pode ser feita por um parâmetro na Query.

Forças

Proposta

Foi proposta em “Defining Design Patterns for IoT APIs” por Rasmus Svensson, Adell Tatrous e Francis Palma.

Problema

A API possui uma grande base de clientes legados que não vão se adaptar a uma refatoração, logo o time de desenvolvimento deve menter diversas versão da mesma API em produção.

Solução

Para se manter diversas versões ativas, existem várias abordagens possíveis. A abordagem mais simples é colocar a versão (ou data) como parâmetro da Query, essa opção é válida somente se a refatoração foi na lógica de negócios ou no corpo da requisição. Essa abordagem não vai funcionar para casos em que a topologia dos end-points foi modificada.

GET /tiquete/6942?versao=2 HTTP 1.1

{
    "id": 6942,
    "titulo": "Erro ao criar Tíquete",
    "descricao": "A API de criação de tíquetes retorna erro 500 quando nenhuma tag é associada.",
    "projeto": "PRJ-001",
    "epico": "CRIAR-TIQUETE",
    "tags": ["Java", "REST", "CRUD"],
    "subtasks": [{
        "id": 6982,
        "title": "Adicionar validação de tags no frontend"
    }, {
        "id": 6983,
        "title": "Adicionar validação de tags no backend"
    }]
}

A outra possibilidade é colocar a versão como um nó do caminho Base. Esse nó pode ser construído iniciando com o caractere v seguido do número da versão para se diferenciar de um identificador. Esse número pode ser um inteiro sequencial ou seguir o formato Semantic Versioning.

GET /v2/tiquete/6942 HTTP 1.1

{
    "id": 6942,
    "titulo": "Erro ao criar Tíquete",
    "descricao": "A API de criação de tíquetes retorna erro 500 quando nenhuma tag é associada.",
    "projeto": "PRJ-001",
    "epico": "CRIAR-TIQUETE",
    "tags": ["Java", "REST", "CRUD"]
}

Recursos versionados

Nesse padrão, os recursos podem ter versões. Quando um recurso é alterado, as versões antigas dele podem ser acessadas.

Forças

Problema

Todas as informações de um recursos são importantes, nenhuma alteração deve apagar informações e versões antigas do mesmo recurso devem estar disponíveis através da API.

Solução

Para resolver esse problema, cada end-point de entidade pode aceitar alguns nós informativos. O primeiro deles listará todas as versões disponíveis, e o segundo deles acessará o conteúdo da versão. Assim /tiquete/6942/versoes vai listar as verões e /tiquete/6942/versoes/4 vai acessar o conteúdo da versão 4 desse tíquete.

GET /v2/tiquete/6942/versoes/4 HTTP 1.1

{
    "id": 6942,
    "titulo": "Erro ao criar Tíquete",
    "descricao": "A API de criação de tíquetes retorna erro 500 quando nenhuma tag é associada.",
    "projeto": "PRJ-001",
    "epico": "CRIAR-TIQUETE",
    "tags": ["Java", "REST", "CRUD"]
}

Em extensão a esse padrão, o end-point da entidade pode retornar a versão atual do recurso usando um cabeçalho. Não há nenhum cabeçalho definido pelo protocolo HTTP para definir a versão da entidade; alguns usam o cabeçalho ETag, mas ele não tem esse proposito. Como a versão é específica da lógica da aplicação, poderíamos definir um cabeçalho usando o prefixo X-, mas essa prática foi definida como antipadrão pela RFC-6648. Logo é necessário que esse cabeçalho seja definido pela aplicação como um cabeçalho comum, talvez Entity-Version, usando um número sequencial.

GET /v2/tiquete/6942 HTTP 1.1

Entity-Version: 6

{
    "id": 6942,
    "titulo": "Erro ao criar Tíquete",
    "descricao": "A API de criação de tíquetes retorna erro 500 quando nenhuma tag é associada.",
    "causaRaiz": "Tíquetes sem nenhuma tag associada geram um excessão não tratada na API.",
    "solucaoProposta": "1. Adicionar validação backend\n2.Adicionar validação frontend"
    "projeto": "PRJ-001",
    "epico": "CRIAR-TIQUETE",
    "tags": ["Java", "REST", "CRUD"]
}

Encontrando antipadrões

Agora podemos inverter a pergunta: dados os padrões existente na literatura, quais devemos evitar? Isso é o que chamamos de antipadrões. São soluções comuns para problemas rotineiros, mas que não apresentam um resultado ótimo.

Muitos desenvolvedores têm preconceito com o termo “antipadrão”. Mas eles se tratam de críticas construitivas. Em muitos casos devem ser interpretados como um aviso de cuidado, não como uma proibição categórica. Antipadrões não existem porque alguém não gostou, existem porque foram levantados vários argumentos de que eles não devem ser utilizados em determinados contextos. Por isso, ao apresentar um antipadrão vamos sempre descrever quais são as desvantagens que eles trazem ao serem usados, cabendo ao desenvolvedor ou à desenvolvedora decidir se deve usar ou não.

Fizemos um levantamentos de antipadrões dentro da literatura analisada, os termos em negrito são destaques da própria literatura. Para apresentar, não vamos seguir o modelo dos padrões, vamos apenas apresentar uma lista de desvantagens para cada antipadrão e, em alguns casos, exemplos pessoais que já implementei. Esta é a lista de antipadrões que vamos apresentar:

Quebrando a autodescrição

Desenvolvedores de APIs REST tendem a ignorar os cabeçalhos padrão, formatos e protocolos e criam customizações proprias. Esta prática quebra o caráter autodescritivo e a mensagem contida no cabeçalho. A ausência do caráter autodescritivo limita a reutilização e a adaptabilidade do recurso REST.

Esse erro é muito comum, não podemos discutir as ocorrências deles porque isso seria fruto de uma enorme pesquisa em bases de códigos que pela própria natureza do antipadrão seria impraticável. Mas podemos discutir o motivo que ele acontece.

O gatilho desse antipadrão é composto por dois fatores muito importantes: pouco tempo e pouco conhecimento. Quando um time sem experiência em REST tem pouco tempo para implementar uma funcionalidade, há a tentação de não se fazer um design prévio e um consulta a documentações ou especificações.

Por volta de 2008 trabalhei em um projeto em que era preciso implementar um controle de concorrência e o arquiteto do time propôs que cada recurso deveria ter um campo lastUpdate. Esse campo deveria ser enviado para o formulário de edição e usado durante a operação de atualização. Se o lastUpdate enviado pela requisição fosse diferente do salvo na base de dados, deveria ser exibido um erro para o usuário. Perceba que essa é exatamente a função do cabeçalho ETag, mas na época o framework utilizado não permitia a criação de requisições REST, logo uma aplicação simulava aplicações desktop.

Desvantagens

Esquecendo a Hipermídia

A falta de hipermídia, ou seja, a não vinculação de recursos, dificulta a transição de estado para aplicativos REST. Uma possível indicação deste antipadrão é a ausência de links de URL na representação de origem, o que normalmente restringe os clientes a seguirem os links, ou seja, limita a comunicação dinâmica entre clientes e servidores.

Uma das grandes dificuldades em se usar APIs é a dificuldade cognitiva para entendê-la. Como Florian Haupt analisa, algumas APIs podem ter inúmeras entidades e, quanto maior for a variedade da estrutura, mais difícil será para um cliente conseguir resolver essas entidades. Assim, esse antipadrão se apresenta para APIs públicas como um grande entrave. Um desenvolvedor deve conhecer a API a fundo e programaticamente gerar todas os end-points de entidade. O fornecimento da hipermídia facilita a navegação, assim como pode permitir que novas entidades sejam facilmente introduzidas na API.

Todas as APIs que eu implementei possuíam esse antipadrão, algumas delas eram públicas o que podem ter causado problemas de atualização para versões futuras.

Desvantagens

Ignorando o cache

Clientes REST e desenvolvedores back-end tendem a evitar o cache devido à sua complexidade de implementação. Ao ignorar recursos de cache não usando _Cache-Control: no-cache_ ou _no-store_ e não provendo um ETag no cabeçalho da resposta, evita-se qualquer redução no número de requisições direcionadas para o servidor.

Esse antipadrão é bastante comum, o controle de cache não está no conceito de pronto de muitos projetos ou em muitas projetos de design, até porque em 99% dos projetos de software o cache não é importante. O cache será importante quando o seu projeto de software for usado por muitos clientes. Vale a pena relembrar a frase do Donald E. Knuth “A otimização prematura é a raiz de todos os males (ou pelo menos da maior parte deles) na programação”.

A necessidade de cache deve ser validada em cada projeto. Ao nos depararmos com essa questão devemos fazer duas perguntas:

Desvantagens

Ignorando os MIME Types

O servidor deve representar um mesmo recurso em vários formatos, por exemplo, XML, JSON, PDF etc. Isso permite que clientes, desenvolvidos em qualquer linguagem consumam a API, independente do formato.

Esse antipadrão acontece quando os desenvolvedores back-end geralmente tem uma única representação de recursos ou dependem de seus próprios formatos, o que limita a acessibilidade e a reutilização da API.

Para esse antipadrão, devemos pontuar duas ocorrências. Uma é esquecer completamente de declarar o MIME Type, a outra é somente declarar um MIME Type. No primeiro caso, é um erro que pode acarretar na necessidade da escrita de mais código para quem consome a API, pois a maioria das bibliotecas de clientes HTTP já esperam o MIME Type. Muitos dos frameworks back-end já têm um MIME Type padrão, mas por boa prática é sempre bom declarar todos os formatos aceitos por um end-point.

Quando temos apenas um tipo de MIME Type, a primeira pergunta que devemos fazer é: quantos clientes nossa API vai ter? Se a resposta for contável e estiver todos dentro do controle da equipe de desenvolvimento, tudo bem seguir somente com um formato fora do padrão. Mas se pudermos ter clientes que não conheçemos, vale a pena adicionar diversos formatos, incluindo todos aqueles que já são padrões para APIs.

Desvantagens

Ignorando o Status Code

Apesar de um rico conjunto de códigos de status definidos para vários contextos, os desenvolvedores REST tendem a evitá-los. Em muitos casos não se preocupam com eles, ou, quando muito, apenas usam os mais comuns, a saber 200, 404 e 500. Ou em um caso ainda pior, usam o código de status errado para um determinado tipo de resposta. O uso correto dos códigos de status é muito importante, assim como conhecer os tipos 2xx, 3xx, 4xx e 5xx que adicionam semântica ao protocolo HTTP.

Esse antipadrão é o que mais impacta qualquer consumidor de APIs. O uso do Status Code indica o tipo de resposta que temos, esquecer ele vai implicar em sempre retornar 200, isso pode mascarar erros na requisição ou mesmo erros no servidor. É imperativa a declaração de todos os possíveis erros na documentação, assim como o tratamento deles em código. Se um código não trata todos os erros, é bem provavél que um problema deixe vazar informações do servidor como hostname, endereço de IP ou mesmo uma informação da pilha de execução (stackstrace). Exceções também podem trazer comportamentos adversos, como estouro de pilha ou vazamento de memórias, que podem causar instabilidade no servidor.

Um código de uma API deve ser feito usando programação defensiva, isso significa que quem desenvolve deve ter cuidado ao prever o máximo de exceções possível. Como não conhecemos o consumidor da API, é bem provavél que haja mau uso dela. Não estou me referindo ao mau uso intencional, mas se deixarmos de validar todos os parâmetros, podemos trazer instabilidade indesejada a nossa API.

Desvantagens

Mau uso de Cookies

Stateless é uma propriedade do REST a ser seguida. Manter o estado da sessão no lado do servidor é uma má prática e não deve ser feito. Cookies descaracterizam sua API, assim ela não poderá ser chamada de RESTful. O envio de chaves ou tokens no campo de cabeçalho Set-Cookie ou Cookie para a sessão do lado do servidor é um exemplo de uso indevido de cookies, que diz respeito à segurança e privacidade.

Não há muito o que discutir nesse padrão. Se uma requisição REST precisa de uma informação em memória no servidor, algo está errado. A boa prática é que, ou a informação esteja armazenada no banco e seja independente da sessão, ou ela esteja dentro do Token JWT e seja parte do cadastro do usuário. Caso não esteja nessas duas condições temos um sério problema de design. Um desses problemas de design é a tentação de armazenar informações inerente ao front-end no back-end, isso é relativamente fácil de se identificar.

Para sabermos se essa informação é referente ao front-end devemos perguntar se ela se refere à lógica da entidade ou à lógica da interface. Se for a lógica da interface, logo ela pode variar entre os vários clientes disponíveis. Outro sinal de que temos um problema de design é o nível de complexidade. Se o nível de complexidade está aumentando, é hora de pararmos e repensar o design da API.

Desvantagens

Túnel através de GET

Sendo o método HTTP mais fundamental em REST, o método GET recupera um recurso identificado por um URI. No entanto, muitas vezes os desenvolvedores usam apenas este método para realizar qualquer tipo de ação ou operação, incluindo a criação, exclusão ou até mesmo para atualizar um recurso. No entanto, HTTP GET é um método impróprio para qualquer ação diferente de acessar um recurso e não corresponde ao seu propósito semântico, se usado indevidamente.

Em APIs REST a semântica dos verbos HTTP deve ser respeitada, não somente pela semântica, mas característica de cada métodos. O método GET não aceita corpo da mensagem, logo se formos usá-lo para alterar recursos, teremos um sério problema de design que dificultará o desenvolvimento. Qualquer alteração requer parâmetros, e eles deverão ser enviados ou por Query ou pelo próprio Path, isso dificultará tanto o desenvolvimento quanto o uso da API.

Como regra última, use: GET deve ser somente usado para acessar informação, nunca para alterar.

Desvantagens

Túnel através de POST

Esse antipadrão é muito semelhante ao anterior, exceto que, além do URI, o corpo da solicitação HTTP POST pode incorporar operações e parâmetros a serem aplicados ao recurso. Os desenvolvedores tendem a depender apenas do método HTTP POST para enviar qualquer tipo de solicitação ao servidor, incluindo acesso, atualização ou exclusão de um recurso. Em geral, o uso adequado de HTTP POST é criar um recurso do lado do servidor. Qualquer paramêtro que altere uma requisição de informação deve ser enviado como Query Parameter.

Muito provavelmente vamos ver esse antipadrão sendo aplicado para parâmetros de buscas. Alguns desenvolvedores usam o corpo da mensagem para definir esses parâmetros. Mas podemos ver dois problemas nessa abordagem. O primeiro dos problemas é a dificuldade de se resolver o cache. O método POST não é passível de cache, porque ele já prevê que serão feitas alterações. Usando os parâmetros na Query, todos os componentes de cache conseguirão identificar que uma resposta prévia pode ser usada para resolver a requisisão sem acessar o servidor. O segundo problema é semântico, como já foi discutido previamente. O verbo POST e PUT devem ser usados para alterar informação, caso tenhamos um requisito de negócio que seja necessário usar, prefira usar o padrão Requisição Expressa.

Desvantagens

Autenticação como consulta

Esse antipadrão é muito recorrente. Em muitas API, os parâmetros de autenticação são enviados através de query string. O protocolo HTTP já prevê vários formatos de autenticação e todos eles se baseiam no uso do header Authorization. Ao usar uma query string estamos expondo no log da aplicação qual é o token de cada usuário, pois muitos frameworks expõem a URI sendo acessada no log de execução. Ou estamos desperdiçando implementações padrões para autenticação (tanto para front-end quanto para back-end), tendo que criar código desnecessário para esse formato específico de autenticação;

Desvantagens

A complexidade de uma API

Discutimos muitos sobre padrões e antipadrões, mas continuando na nossa avaliação de uma API, existe algum método para avaliarmos a complexidade de uma API? Na nossa lista de artigos tem um pouco discutido na sessão anterior, nele Haupt analisa a estrutura de uma API considerando os seus recursos e a sua complexidade. Segundo Haupt, a complexidade de uma API depende diretamente do número recursos que ela define, quantos deles são readOnly, a quantidade de recursos raiz e da profundidade dessa API. Com isso podemos levantar alguns parâmetros novos para analisarmos uma API.

O primeiro parâmetro é a quantidade de recursos, APIs complexas tendem a ter mais recursos. Nesse pontos precisamos nos perguntar se a quantidade de recursos usados na nossa API é ótima. No nosso exemplo temos poucos recursos, basicamente tíquetes e usuários. Mas um usuário pode assumir dois papéis diferentes em relação aos tíquetes sendo autor ou responsavel. Ao fazer a escolha por apenas usar a raiz das requisições por usuario, reduzimos o número de recursos da nossa API, evitando complexidade. Essa complexidade desnecessária pode ser compensada usando o padrão Vinculação de entidades, poucas entidades raiz que provê acesso a muitos recursos usando os valores retornados pela própria API.

Quando falamos do número de recursos readOnly, devemos sempre nos perguntar quem gera esses recursos. Se são dados gerados pelo usuário da API, temos um problema de design. Recursos readOnly devem ser metadados da nossa API seguindo o padrão Acessibilidade dos Metadados. Mas esses metadados não podem ser criados sem um padrão, os nós informativos devem ser normalizados, reduzindo o número de recursos da nossa API. Uma atividade que pode ser feita para se reduzir o número de metadados é a catalogação deles. Quais são os metadados que nossa aplicação gera? Há algum padrão? Se houver um padrão, eles podem ser reduzidos.

Já a profundidade da API deve ser reduzida usando o padrão HATEOAS. Esse padrão não foi detalhado neste capitulo, mas podemos entendê-lo como uma derivação do Vinculação de entidades. Segundo Fielding, o HATEOAS é a implementação definitiva do REST, mas isso não significa que uma API REST deve seguir esse padrão. Na verdade não há nenhuma especificação definindo o HATEOAS deve ser implementado detalhadamente. As definições são genéricas e nenhum dos frameworks de mercado provem um padrão simples de se implementrar. Logo nossa definição também será generica, o HATEOAS permite a navegação entre recursos afim de reduzir a complexidade de uma API.

A maturidade de uma API

Nosso último parâmetro para avaliar uma API é questionar se existe algum modelo de maturidade para API. Modelos de maturidades são bons para avaliarmos onde estamos e como podemos melhorar. Eles normalmente são compostos por níveis e requisitos, ao se cumprir todos os requisitos de um nível, podemos afirmar que atingimos aquele nível. Todo modelo de maturidade começa com um nível zero sem nenhuma exigência, em alguns casos esse nível é chamado de caos.

Quando usamos um modelo de maturidade temos uma análise estruturada do que estamos trabalhado, deixamos de ser subjetivos e passamos ser objetivos. Para APIs REST, temos o Richardson Maturity Model. Ele define quatro nívels para uma API REST, mas o prório Fielding deixou claro que o nível 3 é um pre-requisito para chamar uma API de RESTful. Isso não significa que sua API não é REST se não chegou a esse nível, precisamos ser flexíveis pois todo software evolui. Se ela expõe recursos e usa os verbos HTTP, de certa forma ela é um tipo de REST.

Modelo de maturidade de Richardson

No nível zero, podemos qualificar qualquer comunicação HTTP. Existem muitos projetos legados que se encaixam nesse nível, me recordo que pelo ano de 2008, quando comecei a trabalhar com projetos web, era comum encontrar endpoints como /listarUsuarios.do em que os parâmetros da busca eram passados pelo corpo da mensagem.

No nível um, podemos classificar as APIs que tem alguma lógica na identificação das entidades. Assim qualquer esforço para catalogar as entidades podem fazer com que sua API seja mais fácil de se utilizar. Nesse caso, os end-points serão derivações da entidade, por exemplo, a busca de usuários podem ser /usuario/buscar ou somente /usuario.

No nível dois, podemos classificar as APIs que usam os verbos considerando a sua semântica. Nesse nível não vamos ver os seguintes antipadrões Túnel através de GET e Túnel através de POST.

Por fim, no nível três, temos o controle de hípermidia. Uma requisição vai retornar não somente a informação, mas as entidades e ações correlatas e seus respectivos end-points. Nesse nível temos o que conhecemos como HATEOAS (Hypertext As The Engine Of Application State ou Hipertexto como o Motor do Estado do Aplicativo). O back-end ganha um importância meio na aplicação, e o front-end se torna menos acoplado ao back-end. Será possível adicionar novas funcionalidades ao front-end sem nenhuma alteração no mesmo.

Para alcançar o nível 3, é necessário um esforço grande de design que deve ser alinhado tanto com todos os consumidores da API. A implicação de criar esse nível de API significa que o consumidor vai navegar no resultado, não somente usar a API.7

Conclusão

Neste capítulo, tentamos definir como podemos avaliar uma API. O primeiro elemento para classificação são padrões e antipadrões de projetos. Com eles podemos aumentar o nosso repertório para classificar e descrever APIs. Será que nossa API segue padrões de mercado? Será quem um dos padrões pode ser aplicado para resolver um problema que temos?

Depois avaliamos como se classifica a complexidade de uma API. APIs devem ser de fácil compreensão, quando elas se tornam muito complexas, ou elas não deveriam ser uma API REST, ou elas precisam ser retrabalhadas. Existe algum recurso desnecessário? Podemos extrair um recurso novp? Podemos aplicar algum padrão? APIs complexas podem dificultar a implementação de cliente ou de servidores, e isso pode ser resolvido com uma refatoração da API.

Por fim, podemos analizar a maturidade de uma API. Existem um método para isso? Vimos que sim, podemos qualificar qualquer API em níveis. Isso não significa que nossa API precisa ter o nível máximo, mas que com ela podemos ver como podemos adicionar mais funcionalidades a nossa API.

Licença Creative Commons
Este obra está licenciado com uma Licença Creative Commons Atribuição-NãoComercial-CompartilhaIgual 4.0 Internacional .
Escrito em 16/janeiro/2022