Tudo o que sei sobre um bom design de API
(seangoedecke.com)- Em engenharia de software, APIs são uma ferramenta central, e uma característica desejável de uma boa API é ser familiar e simples a ponto de parecer entediante
- Como uma API, depois de publicada, é difícil de mudar, o princípio de não quebrar o ambiente do usuário (WE DO NOT BREAK USERSPACE) é importante
- Quando a mudança for inevitável, é necessário versionamento (versioning), mas isso é um mal necessário que aumenta bastante a complexidade e o custo de manutenção
- A qualidade da API acaba dependendo do valor do próprio produto, e um produto mal projetado dificulta a criação de uma boa API
- Para estabilidade e escalabilidade, vale considerar autenticação baseada em chave de API, idempotência, rate limiting e paginação baseada em cursor
Introdução: a importância e o contexto do design de APIs
- Uma das principais atividades de engenheiros de software modernos é interagir com APIs
- O autor também tem experiência em projetar/implementar/utilizar APIs públicas e internas em várias formas, como REST, GraphQL e ferramentas de linha de comando
- Os conselhos existentes sobre design de APIs tendem a se apegar a conceitos complexos (definição de REST, HATEOAS etc.)
- Este texto organiza princípios práticos de design de APIs com base em experiência real
Equilíbrio entre familiaridade e flexibilidade: a primeira condição de uma boa API
- Uma boa API é uma API “comum e entediante”, ou seja, seu modo de uso deve ser parecido com o de APIs que as pessoas já encontraram antes
- Como o usuário quer se concentrar em atingir o próprio objetivo, e não na API em si, é preciso um design com baixa barreira de entrada
- Uma vez publicada, uma API é muito difícil de alterar, então é preciso cuidado desde a fase inicial de projeto
- Desenvolvedores querem APIs o mais simples possível, mas sempre existe a preocupação de preservar flexibilidade no longo prazo
- No fim, a questão central é o equilíbrio entre familiaridade e flexibilidade de longo prazo
Nunca quebrar o espaço do usuário (WE DO NOT BREAK USERSPACE)
- Em geral, mudanças que adicionam campos à estrutura de resposta não causam problema
- Já remover campos ou mudar tipos e estruturas acaba quebrando o código de todos os consumidores
- Quem mantém a API tem a responsabilidade de não estragar deliberadamente o software dos usuários existentes
- Até o erro de grafia no header HTTP "referer" não é corrigido por causa dessa cultura de preservar o espaço do usuário
Como mudar sem quebrar a API: estratégia de versionamento
- Mudanças quebrando compatibilidade na API só devem ser permitidas quando forem realmente necessárias, e nesse caso a resposta é versionamento
- É preciso operar a versão antiga e a nova ao mesmo tempo, induzindo uma transição gradual
- Identificadores de versão podem ser usados de várias formas, como URL (
/v1/) ou headers, e os usuários podem migrar no próprio ritmo - O versionamento tem desvantagens como enorme custo de manutenção (mais endpoints, testes, suporte) e confusão para o usuário
- Mesmo usando uma camada interna de tradução como a Stripe, a complexidade fundamental não pode ser evitada
- Introduzir versionamento na API deve ser o último recurso
O sucesso da API depende totalmente do valor do produto
- Uma API é, em essência, apenas a interface de um produto de negócio real
- Mesmo APIs como OpenAI e Twilio são, no fim, casos em que o usuário queria a funcionalidade em si oferecida pela API
- Se o produto tiver valor, as pessoas o usarão mesmo que a API seja incômoda
- A qualidade da API é uma característica “marginal”: só vira fator de escolha quando a competitividade essencial é parecida
- Por outro lado, um produto sem API é uma grande barreira para usuários técnicos
Se o design do produto for ruim, a API também não pode ser boa
- Mesmo que exista uma API tecnicamente muito bem feita, isso não significa muito se o produto não tiver apelo de mercado
- Mais importante ainda: se a estrutura básica dos recursos for ilógica ou ineficiente, isso aparece na API também
- Por exemplo, um sistema que armazena comentários como lista encadeada dificulta até mesmo um design RESTful natural
- Problemas técnicos que podem ficar escondidos na UI ficam todos expostos na API, forçando desnecessariamente o usuário a entender o sistema interno
Autenticação (Authenticaton) e diversidade de usuários
- É preciso oferecer suporte obrigatório a autenticação baseada em chaves de API de longa duração
- Mesmo que se dê suporte adicional a métodos mais seguros como OAuth, a barreira de entrada da chave de API é muito menor
- Consumidores de API não são apenas engenheiros; há também não desenvolvedores (vendas, planejamento, estudantes, hobbyistas etc.)
- Exigências de autenticação difíceis ou complexas (como OAuth) viram barreira para usuários não especialistas
Idempotência e tratamento de retries
- Em requisições de ação (por exemplo, pagamento, mudança de estado etc.), é importante garantir segurança para retry em caso de falha
- Idempotência significa garantir que, mesmo enviando a mesma requisição várias vezes, o resultado seja processado apenas uma vez
- O método padrão é passar uma "chave de idempotência" em parâmetro ou header para evitar processamento duplicado
- Para armazenar a chave de idempotência, um armazenamento simples de chave/valor como Redis é suficiente, e na maioria dos casos dá para aplicar expiração periódica sem problema
- Em geral, isso não é necessário para requisições de leitura/remoção (no estilo REST)
Segurança da API e rate limiting
- Requisições de API por código podem acontecer muito mais rápido do que interações manuais de usuários
- Uma única API publicada sem muita atenção pode acabar sendo usada de forma inesperada (por exemplo, em um sistema de chat em larga escala)
- Rate limiting é indispensável, e deve ser aplicado de forma mais rígida em operações com custo elevado
- Uma desativação temporária da API para um cliente específico (killswitch) também deve ser considerada como opção
- É preciso informar dados de rate limiting por meio de headers de resposta como
X-Limit-RemainingeRetry-After
Estratégia de paginação
- Para retornar grandes conjuntos de dados com eficiência (por exemplo, milhões de tickets), paginação é essencial
- Paginação baseada em offset é simples, mas vai ficando lenta em grandes volumes de dados
- Paginação baseada em cursor funciona bem até em datasets muito grandes, sem degradação da performance da query
- A abordagem com cursor é um pouco mais difícil de implementar e usar, mas no longo prazo tem grande chance de se tornar uma mudança obrigatória
- É sensato incluir no response um campo como
next_page, orientando claramente qual cursor usar na próxima requisição
Campos opcionais e opinião sobre GraphQL
- Campos custosos ou lentos devem ficar fora da resposta padrão e ser adicionados opcionalmente só quando necessário
- É possível incluir dados relacionados com um parâmetro como
includes - GraphQL tem a vantagem da flexibilidade na estrutura de dados, mas traz problemas como menor acessibilidade para não desenvolvedores, maior complexidade de cache/casos de borda e maior dificuldade de implementação no backend
- Pela experiência prática, adotar GraphQL é apropriado apenas quando realmente necessário
Características de APIs internas
- APIs internas têm várias condições diferentes das APIs externas (públicas)
- Como os consumidores são em sua maioria engenheiros de software profissionais, autenticação mais complexa e mudanças incompatíveis podem ser aceitáveis
- Ainda assim, os princípios de projeto voltados a idempotência, prevenção de incidentes e redução da carga operacional continuam válidos
Resumo
- APIs têm a característica de serem difíceis de mudar e fáceis de usar
- Não quebrar o espaço do usuário é o dever mais importante de quem mantém uma API
- Versionamento de API tem custo alto, então deve ser usado apenas como último recurso
- No fim, a qualidade da API é determinada pelo valor essencial do produto
- Um produto mal projetado tem limites grandes, mesmo que se tente compensar isso no nível da API
- É importante oferecer métodos simples de autenticação, garantir idempotência em requisições de ação indispensáveis e adotar medidas de estabilidade como rate limiting e paginação
- APIs internas têm estratégias diferentes conforme o uso e o público, mas ainda exigem cuidado no design
- REST, JSON, OpenAPI e formatos semelhantes não são o ponto essencial. Uma documentação clara é mais importante
1 comentários
Opinião no Hacker News
O conselho “nunca quebre o
userspace” é famoso, mas o texto também destaca bem o lado oposto disso. Ou seja, “a API do kernel pode quebrar sem aviso”. O ponto importante não é “nunca quebre nenhuma API de jeito nenhum”, mas sim o equilíbrio sutil de “nunca quebre apenas as partes cuja estabilidade foi declarada”Mesmo que o kernel do Linux não quebre o
userspace, a GNU libc quebra a compatibilidade deuserspacecom bastante frequência. Então, no fim das contas, o espaço de usuário do Linux quebra com frequência, por mais que os desenvolvedores do kernel se esforcem. Programas e bibliotecas compilados com versões novas da libc às vezes não rodam direito em versões antigas da libc, então na prática é preciso atualizar todos os componentes de uma vez. De forma meio irônica, o Windows já resolveu esse problema há décadas com o modelo de redistributablesÉ bem conhecido que o Linux não tem uma API pública estável para drivers, e já ouvi dizer que esse foi justamente um dos motivos para o Google desenvolver o Fuschia OS. O Linux acaba tendo direções diferentes para o espaço de usuário e para o hardware
O autor parece não gostar muito de APIs baseadas em versão, mas eu sempre recomendo adotar versionamento desde o início ao criar um app. Como não dá para prever o futuro, em algum momento mudanças incompatíveis por fatores externos inevitavelmente vão acontecer com você também
Na verdade, acho que o próprio autor recomendou versionamento. No texto ele diz que “versões são uma forma responsável de mudar uma API”, então no fim está incentivando o versionamento em si. Só que a migração para uma nova versão deve ser o último recurso
Concordo com a opinião de que não se deve colocar
v1no endpoint sem necessidade. O que normalmente acontece conforme a API cresce é que primeiro se tenta preservar compatibilidade adicionando campos ou opções aos endpoints existentes. E, quando surge a necessidade de algo realmente incompatível, geralmente se dá um nome novo ao próprio endpoint e se cria um endpoint totalmente novo (não/v2). Se for preciso mudar a API inteira, o serviço antigo costuma ser descontinuado e um serviço completamente diferente é lançado, com outro nome desde o início. Em 25 anos de trabalho, vi um serviço usando/v1e/v2lado a lado exatamente uma vezNão acho que a intenção do autor seja dizer que nunca se deve colocar
/v1no endpoint desde o começo. O ponto é que você deve fazer o máximo possível para evitar que surja um novo/v2. Quando aparece um/v2, cada correção de bug exige alterações de código nos dois lados, os condicionais crescem exponencialmente e a base de código vira um espaguete. No fim, o projeto original de/v1que passou a suportar múltiplas versões foi pouco cuidadoso com compatibilidade futuraAcho que não há problema nenhum em adicionar versionamento depois. Por exemplo, começar com
/api/postse depois acrescentar a próxima versão como/api/v2/postsjá é suficienteNão concordo com a abordagem de embutir a versão desde o começo. Isso faz com que o uso de múltiplas versões se torne realmente comum, e eu acho que isso é pior, não melhor
Este texto foi muito útil. Eu acrescentaria mais um conselho: a qualidade de uma API é inversamente proporcional à dificuldade de obter sua documentação. Se você só consegue a documentação depois de assinar um contrato, pode assumir com segurança que a qualidade dessa API será péssima
O autor disse para salvar a chave de idempotência em um armazenamento key/value como Redis em vez de colocá-la separadamente na tabela de comentários, mas fico em dúvida se isso garante idempotência de forma confiável em todos os casos de falha. Por exemplo, se o servidor faz uma escrita condicional como
SET key 1 NXe encontra a chave já existente, ele deveria simplesmente pular a criação do comentário, mas nesse ponto talvez a requisição anterior ainda nem tenha sido refletida de fato no banco. O armazenamento da chave de idempotência precisa ser confirmado junto com a operação real na mesma transação e, se necessário, também revertido. No fim, a essência da chave de idempotência é virar “o identificador único desta operação ou requisição”. Por exemplo, ela deveria ser um identificador por recurso adequado a cada caso, como “criação de comentário” ou “atualização de comentário”A vantagem da paginação baseada em cursor é que, do ponto de vista do usuário, mesmo que novos itens sejam adicionados entre carregar uma página e clicar em “próximo”, ele não precisa ver de novo os itens que já viu. O método com cursor guarda o ID do último objeto da página anterior e entrega os itens seguintes, por isso é especialmente útil em scroll infinito. Por outro lado, a desvantagem é que fica difícil implementar a função de “pular para a página N” com paginação por cursor
Hoje em dia, quando se fala em “API”, a maioria pensa em enviar uma requisição para um webapp, definir parâmetros e headers e buscar dados, mas originalmente API significa “Application Programming Interface”, isto é, “interface de programação de aplicações”. O termo começou a ser usado nos anos 1940 e, até os anos 1990, era usado quase sem outro significado. A história das APIs já passa de 80 anos, e existe muito material antigo excelente. Refletir sobre quais problemas os programadores da época enfrentavam e como os resolviam provavelmente pode ajudar bastante também hoje
Não concordo com a ideia de tratar usuários internos apenas como “usuários”. Embora sejam pessoas mais técnicas e com maior probabilidade de serem programadores, elas também vivem ocupadas e muitas vezes não têm tempo nem folga para reagir a mudanças na API porque estão focadas nos próprios projetos. Se possível, é importante fazer bastante teste de
dogfoodingdentro da equipe antes de abrir para fora. Depois que algo é exposto externamente, a promessa de “não quebrar ouserspace” precisa ser cumprida sem faltaNo caso de usuários internos, normalmente já existem ferramentas de instrumentação implementadas para contatá-los diretamente e conduzir a migração. Graças a isso, também dá para descontinuar versões de API, então adotar versionamento de forma estratégica é bastante atraente. Já participei de versionamento real de APIs e vi claramente os benefícios em comparação com organizações que basicamente não usam isso
Acho que uma estratégia de versionamento ajuda a resolver esse problema. Uma das melhores formas de considerar os usuários internos é colaborar na especificação e compartilhar com os interessados até mesmo a versão em andamento dessa especificação. Mesmo que a documentação esteja sempre mudando, ter um ponto de referência facilita muito o feedback interno e externo, e isso pode ser extremamente útil desde que se evitem apenas riscos de conflitos de política
Em vez de salvar a chave de idempotência no Redis, acho mais confiável, sempre que possível, salvá-la junto na mesma transação em que os dados reais são gravados
O alerta “nunca quebre o
userspace” é realmente importante. Foi triste ver Spotify, Reddit e Twitter ignorarem esse princípio recentementeComo referência, recomendo também o link https://jcs.org/2023/07/12/api, que reúne boas recomendações sobre APIs