No post anterior tive a oportunidade de falar de uma forma mais abrangente sobre o API-First. O exercício é interessante, uma vez que é possível focar no conceito, ao invés de ferramentas e processos. Mas o objetivo desse artigo é cairmos de cabeça em como exercitar a ideia de forma prática.
O problema a ser solucionado será explicado mais abaixo, mas antes de cairmos na tentação de pensar no framework web, na modelagem de dados, em qual servidor de aplicação usar, ou em virtualizar com Docker e Kubernetes, vamos focar primeiro na especificação.
Com sorte seremos capazes de produzir uma API que atenda a necessidade dos nossos stakeholders.
Vamos "reciclar" o problema proposto no post sobre Django REST Framework e construir um "Feedly-clone". Então, partiremos da análise do seguinte requisito funcional:
Eu como usuário, Quero poder visualizar notícias recentes de websites que me cadastrei, Para que assim eu fique atualizado sobre assuntos de meu interesse
Com essa user story, podemos pensar no processo que envolve a prática do API-First.
No post anterior mencionei alguns passos que não podem faltar em um contexto de API-First. Jennifer Riggins, no "How to Design Great APIs with Api-First Design" traz uma visão mais prática sobre como o processo pode ser definido:
Vamos esmiuçar de forma prática cada um dos passos acima.
A abordagem adotada nesse post seguirá valores apresentados no "Three Principles of API First Design". Mais especificamente:
Your API comes first, then the implementation
E nesse exemplo vamos separar especificação da implementação de forma "concreta". Ou seja, serão dois artefatos independentes.
Existem alternativas a esse approach, por exemplo, o uso do drf-yasg. Esta é uma ferramenta que gera a especificação (Swagger e OpenAPI Spec) automaticamente de acordo com o código fonte do seu projeto Django (com REST Framework).
Algo deve ser muito claro independente do que optar: Só deve haver uma verdade absoluta; No caso desse artigo, será a especificação escrita à parte.
Vamos usar REST como padrão de comunicação entre o nosso servidor e os clientes. Portanto, poderemos utilizar Open API Spec para especificar a API. Caso a solução fosse a adoção de outra tecnologia (por exemplo, GraphQL ou gRPC), alternativas para o passo de descrição e documentação da interface devem ser aplicadas.
Além disso, seremos o mais "compliant" possível com o RESTful, ou seja, seguiremos à risca recomendações
em relação a path, status codes e verbos. O versionamento da API acontecerá via fragmento no path (ex.: /v1/
),
e a princípio apresentaremos apenas respostas em JSON.
Imaginemos que depois de ponderar com outros stakeholders, chegamos à seguinte conclusão:
[
{
"id": 1,
"title": "Título do artigo publicado no canal ABC",
"pub_date": "data-com-timezone-incluso",
"summary": "Resumo do artigo",
"content": "Conteúdo completo do artigo",
"image": "imagem-principal-do-artigo.png",
"channel": "ABC",
"url": "http://article-1.html"
},
{
"id": 2,
"title": "Título do artigo publicado no canal XYZ",
"pub_date": "data-com-timezone-incluso",
"summary": "Resumo do artigo (2)",
"content": "Conteúdo completo do artigo",
"image": "imagem-principal-do-artigo-2.png",
"channel": "XYZ",
"url": "http://article-2.html"
}
]
Além disso, esboçamos as seguintes rotas:
GET /api/v1/channels/<id-do-channel>
: Pega a lista de artigosGET /api/v1/articles/<id-do-artigo>
: Pega o artigo completoVamos ocultar complexidades fora do escopo da user story apresentada (como por exemplo, paginação e
autenticação). Além disso, propositalmente não definimos um formato "oficial" para pub_date
, apenas
determinamos que deverá ser uma data em UTC.
Por tratar-se de um esboço inicial que gerará material para a definição do style guide da API e dos endpoints em si, é natural deixarmos passar alguns detalhes que podem gerar problemas de integração.
Abaixo, o documento representando exatamente o que foi acordado na seção anterior, com OpenApi Specification:
openapi: "3.0.2"
info:
title: Feedly-clone
version: v1
paths:
/channels:
summary: Set of channels, e.g. blogs, websites or podcasts
get:
responses:
"200":
description: List of channels
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Channel"
/channels/{id}:
parameters:
- $ref: "#/components/parameters/id"
summary: Details of a channel
get:
responses:
"200":
description: Channel detail response
content:
application/json:
schema:
$ref: "#/components/schemas/Channel"
/articles:
summary: Set of items
get:
responses:
"200":
description: List of articles
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Article"
/articles/{id}:
parameters:
- $ref: "#/components/parameters/id"
summary: Details of an article
get:
responses:
"200":
description: Article detail response
content:
application/json:
schema:
$ref: "#/components/schemas/Article"
components:
parameters:
id:
name: id
in: path
required: true
schema:
type: integer
schemas:
Channel:
properties:
id:
type: integer
name:
type: string
Article:
properties:
id:
type: integer
title:
type: string
pub_date:
type: string
summary:
type: string
content:
type: string
image:
type: string
channel:
type: string
url:
type: string
O artigo "Swagger na prática" pode ser de ajuda caso você não esteja familiarizado com a síntaxe acima.
A partir daqui podemos pensar sob a ótica de casos de uso. Práticas e ferramentas podem variar dependendo da sua criatividade e do seu time. Para fins didáticos vamos manter esse passo o mais simples possível.
Para início de conversa o path articles/
não está alinhado o suficiente com a semântica por trás do problema. O formato RSS chama o que estamos chamando de Article
de Item
, logo,
se quisermos manter uma linguagem ubíqua é fundamental renomearmos esse elemento.
Outro ponto de controvérsia em nosso esboço é que o recurso articles/
poderia ser um nested resource de channels/{id}/
:
channels/{id}
: Detalhes do channelchannels/{id}/items
: Lista de items daquele channelchannels/{channel-id}/items/{item-id}
: Detalhes do itemDigamos que após consultar os stakeholders de mobile e web, eles concordem com a alteração. É exatamente esse tipo de questionamento que esse passo deve trazer, além de instigar a discussão com demais interessados.
Por último, mas não menos importante, vamos supor que nossos possíveis clientes não estejam propensos a mostrar
o conteúdo completo dos artigos em sua listagem. Logo, eles estariam fazendo "over-fetching" de informação. Podemos
remover esse campo (content
) de channels/{id}/items/{id}
.
A especificação final você pode conferir aqui.
Over-fetching (trazer mais conteúdo do que o necessário em um request) e under-fetching (trazer menos conteúdo do que o necessário) são problemas comuns em APIs REST que possuem pluralidade de clientes.
Soluções variam, das mais ingênuas (criar um endpoint para mobile e um para web, por exemplo) até as mais rebuscadas (como deixar o REST de lado e adotar GraphQL). O "meio-termo" seria a adoção de algum mecanismo que permita ao cliente especificar quais campos ele deseja carregar na requisição.
E é exatamente isso que o FlexFields faz para soluções escritas em REST Framework.
É hora de validarmos o contrato.
Poderíamos escrever views muito simples em Django (por exemplo) que atendam a especificação ao responder conteúdo estático. Mas possivelmente a melhor forma de executarmos esse passo seja através de ferramentas chamadas mock servers.
As opções são as mais variadas, em diferentes linguagens. A minha sugestão é o Prism, uma ferramenta escrita em Javascript, fácil de instalar e usar.
Executando o Prism com a especificação passada como argumento, temos um servidor fornecendo o contrato via REST:
$ prism mock openapi.yml
› [CLI] … awaiting Starting Prism…
› [CLI] ℹ info GET http://127.0.0.1:4010/channels
› [CLI] ℹ info GET http://127.0.0.1:4010/channels/869
› [CLI] ℹ info GET http://127.0.0.1:4010/channels/857/items
› [CLI] ℹ info GET http://127.0.0.1:4010/channels/226/items/12
› [CLI] ▶ start Prism is listening on http://127.0.0.1:4010
$ curl http://127.0.0.1:4010/channels/1/items/1
{"id":1,"title":"Anthony Mackie fala sobre se tornar o Capitão América","pub_date":"Sat, 29 Feb 2020 19:21:37 +0000","summary":"Quero que o meu Capitão América represente todo mundo, não só um grupo específico de pessoas","image":"https://uploads.jovemnerd.com.br/wp-content/uploads/2020/02/anthony-mackie-capitao-america.jpg","content":"Conteúdo completo","url":"https://jovemnerd.com.br/nerdbunker/anthony-mackie-fala-sobre-se-tornar-o-capitao-america/"}
Isso permite que novas avaliações sejam realizadas, modificações aconteçam com antecedência, casos de uso sejam testados, e até mesmo que outros stakeholders comecem a desenvolver a parte deles sem precisar esperar pelo backend todo estar pronto.
Nesse passo estruturamos o release da especificação, gerenciamos o processo e futuras iterações, e (claro) versionamos o arquivo.
Enquanto trabalhei na Loadsmart, mantínhamos a especificação em um repositório Git e tínhamos
um build no CI para publicar a documentação baseada na versão do arquivo que encontrava-se no master
.
Possuímos várias opções para esse passo. Talvez o ReDoc seja a ferramenta mais estilosa para gerar documentação a partir do OpenAPI.
Para fins didáticos vou usar a ferramenta do Swagger e expor a documentação no SwaggerHub: https://app.swaggerhub.com/apis-docs/kplaube/feedly-clone/v1
Teste sempre. Automatize sempre que possível.
Nessa etapa validaremos e garantiremos o funcionamento do contrato. Casos de uso podem ser necessários para estressar a coerência da API. Particularmente, não acho que essa seja a camada mais interessante para testar coisas além do contrato estabelecido (como por exemplo, a própria lógica de negócio). Pode tornar-se uma linha tênue em sua pirâmide de testes.
O Dredd é uma ferramenta bem interessante para testar a especificação construída. Ela utiliza um servidor como alvo, portanto, podemos adicioná-la ao build pipeline, levantarmos o nosso serviço, e ela operará como uma espécie de Component Test.
Executando contra o nosso servidor mockado, tudo deveria funcionar "out of the box":
$ dredd ./openapi.yml http://127.0.0.1:4010
warn: API description parser warning in /Users/klaus/Desktop/openapi.yml:66 (from line 66 column 7 to column 13): 'Parameter Object' contains unsupported key 'schema'
warn: API description parser warning in /Users/klaus/Desktop/openapi.yml:73 (from line 73 column 7 to column 13): 'Parameter Object' contains unsupported key 'schema'
warn: API description parser warning in /Users/klaus/Desktop/openapi.yml:102 (from line 102 column 7 to column 12): 'Schema Object' contains unsupported key 'allOf'
pass: GET (200) /channels duration: 63ms
pass: GET (200) /channels/1 duration: 20ms
pass: GET (200) /channels/1/items duration: 14ms
pass: GET (200) /channels/1/items/1 duration: 11ms
complete: 4 passing, 0 failing, 0 errors, 0 skipped, 4 total
complete: Tests took 116ms
Um outro extra muito interessante é a possibilidade de escrever hooks em Python, o que nos permite criar casos de uso de acordo com diferentes aspectos e contextos.
Agora sim! Finalmente a parte divertida.
Agora é correr para o Django e implementar a API. Se você estiver interessado no Django REST Framework, escrevi sobre ele recentemente no "Construindo APIs em Django com Django REST Framework". Não perca.
Ao fim do desenvolvimento, não esqueça de executar o Dredd apontando para o serviço Django. Aliás, colocar essa checagem em um CI é uma excelente ideia! Dependendo do seu release train, a falha dessa checagem pode inclusive bloquear o seu Continuous Delivery.
A sua funcionalidade está pronta. O seu produto está documentado. Agora é hora de "ir para a rua".
Com essa ideia de "uma API para todos governar", fica mais fácil ser adepto da corrente do "eat your own dog food". Tendo outros times da sua própria organização como usuários fica mais fácil coletar feedbacks e reiniciar todo o processo descrito com novas demandas e possíveis correções.
Para terceiros, é fundamental tratar a API como parte do negócio, e ela (e sua documentação) precisa ter espaço de destaque no website da sua empresa. O Twillio faz isso muito bem:
Além disso, existem marketplaces de APIs espalhados pela web que podem servir como canal de propaganda da sua API. O RapidAPI é um bom exemplo de plataforma para conectar-se com possíveis consumidores.
Não esqueça que propiciar SDKs para os seus consumidores faz parte dessa etapa. Não basta apenas escrevermos o backend, precisamos nos engajar em escrever clientes para diferentes linguagens. O Swagger Codegen pode ajudar nessa tarefa, gerando SDKs a partir da especificação da sua API.
Abordamos o processo, os passos, e quais ferramentas podemos utilizar para atender os requisitos necessários em cada etapa. Faltou abordar de forma prática como "colocar isso tudo junto para funcionar". O post ficou gigante, e possivelmente abordaremos em uma parte 2 como colocar isso tudo em um CI, e em como orquestrar release de especificação e deploy de aplicação.
Há uma pluralidade interessante de ferramentas e serviços que giram em torno do conceito de API-First. Se por um lado isso pode significar aumentar a complexidade do seu processo de desenvolvimento, por outro é também uma certa garantia de que existem outros players investindo nessa prática, e que se você tem na sua API o seu principal produto, talvez ficar por fora possa ocasionar um problema de estratégia.
Até a próxima.