Neste artigo, vamos explorar os 7 principios fundamentais, com codigo real, exemplos de empresas como Stripe, GitHub e Twilio, e uma perspectiva sobre como a era da IA esta mudando as regras do jogo.

Recursos selecionados para complementar sua leitura
AI Architect
Ajudo founders a parar de lutar com decisões de arquitetura e começar a entregar. Após 10+ anos construindo produtos em três continentes, aprendi que o melhor código é invisível—ele funciona, escala e converte. Pós-graduado em arquitetura de software e soluções. Conecto profundidade técnica com resultados de negócio para entregar produtos que as pessoas realmente usam. Também mentoro desenvolvedores e criadores em programas ao vivo, podcasts e iniciativas de comunidade focadas em tecnologia inclusiva.
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Continue explorando tópicos similares
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.
Consultorias modulares para founders e CTOs fracionados. Você recebe diagnóstico acionável e acompanhamento direto comigo.
2 vagas para consultorias no Q2
Em 2019, uma fintech brasileira com 200 clientes integrados decidiu "melhorar" sua API de pagamentos. O time de backend — talentoso, bem-intencionado — renomeou campos, mudou a estrutura de resposta de erros e alterou o formato de datas. Tudo em uma unica release. Em uma segunda-feira.
Na quarta-feira, 143 integracoes estavam quebradas. O Slack do suporte tinha 400+ mensagens. Tres clientes enterprise iniciaram processo de migracao para concorrentes. O CEO estava em uma call com o VP de Engenharia tentando entender como uma "melhoria tecnica" tinha se transformado em uma crise de negocios.
O problema nao era tecnico. O codigo novo era objetivamente melhor. O problema era conceitual: o time tratou a API como codigo interno. Como se fosse um modulo que so eles usavam. Mas uma API publica nao e codigo. E um contrato. E contratos quebrados destroem relacoes.
Essa historia nao e unica. Ela acontece toda semana em empresas de todos os tamanhos. E acontece porque a maioria dos desenvolvedores nunca aprendeu os principios de design de API que separam sistemas que duram uma decada de sistemas que quebram em meses.
Esses principios nao sao novos. Eles nao sao complicados. Mas sao sistematicamente ignorados.
Neste artigo, vamos explorar os 7 principios fundamentais, com codigo real, exemplos de empresas como Stripe, GitHub e Twilio, e uma perspectiva sobre como a era da IA esta mudando as regras do jogo.
Antes dos principios, precisamos falar de dinheiro. Porque e isso que convence stakeholders.
Uma breaking change em API publica tem custos em tres dimensoes:
1. Custo direto de retrabalho Cada cliente integrado precisa atualizar seu codigo. Se voce tem 500 clientes e cada um gasta 4 horas de dev para adaptar, sao 2.000 horas de trabalho que voce impos ao ecossistema. A R$150/hora (conservador para senior), sao R$300.000 em custo que voce exportou para seus clientes.
2. Custo de confianca
Clientes que integram com sua API tomam uma decisao de dependencia. Eles apostam que voce nao vai quebrar o contrato. Quando voce quebra, a confianca e destruida de forma desproporcional ao tamanho da mudanca. Um campo renomeado de user_name para username pode parecer trivial — mas para o cliente que tem esse campo hardcoded em 15 microservicos, e uma crise.
3. Custo de churn Estudos internos de plataformas como Twilio e Stripe mostram que breaking changes sao o segundo maior driver de churn em APIs de plataforma — atras apenas de downtime. E diferente de downtime, breaking changes sao percebidas como intencionais. O cliente pensa: "eles sabiam que isso ia me afetar e fizeram mesmo assim."
O Lindy Effect funciona ao contrario aqui: quanto mais tempo sua API existiu sem breaking changes, mais confianca ela acumulou. E quanto mais confianca, mais integracoes. E quanto mais integracoes, mais custosa sera qualquer mudanca futura. Esse ciclo e inevitavel — entao e melhor comecar certo.
Este e o erro mais comum e mais destrutivo. O dev olha pra tabela do banco e cria um endpoint que e basicamente um SELECT * com JSON em volta.
// GET /api/tbl_orders/12345
{
"id": 12345,
"usr_id": 789,
"prd_id_fk": 456,
"ord_status_cd": 2,
"crt_dt": "2026-02-08T10:30:00",
"upd_dt": "2026-02-08T11:00:00",
"amt_total_cents": 15000,
"shipping_addr_id_fk": 321,
"is_del": 0
}
O que esta errado aqui? Tudo.
usr_id, prd_id_fk, crt_dt)ord_status_cd: 2)is_del)// GET /api/orders/12345
{
"id": "ord_12345",
"status": "processing",
"customer": {
"id": "cus_789",
"name": "Maria Silva",
"email": "maria@exemplo.com"
},
"items": [
{
"id": "item_001",
"product": {
"id": "prod_456",
"name": "Plano Enterprise"
},
"quantity": 1,
"unit_price": {
"amount": 15000,
"currency": "BRL",
"formatted": "R$ 150,00"
}
}
],
"total": {
"amount": 15000,
"currency": "BRL",
"formatted": "R$ 150,00"
},
"shipping_address": {
"street": "Rua Augusta, 1234",
"city": "Sao Paulo",
"state": "SP",
"zip": "01310-100"
},
"created_at": "2026-02-08T10:30:00Z",
"updated_at": "2026-02-08T11:00:00Z"
}
A diferenca e dramatica. Observe:
ord_, cus_, prod_) — padrao que a Stripe popularizou. Voce olha pro ID e sabe o tipo do recurso sem contexto adicional."processing" em vez de 2. O consumidor nao precisa de tabela de lookup.Z.Pergunte: "Se eu nunca tivesse visto o banco de dados, essa resposta faria sentido?"
Se a resposta for nao, voce esta vazando implementacao. E implementacao muda. Contratos nao deveriam.
A Stripe e o exemplo definitivo aqui. A API deles sobrevive ha mais de 10 anos sem grandes breaking changes. Um dos motivos: a camada de API e completamente desacoplada do banco. Eles poderiam migrar de PostgreSQL para qualquer outra coisa amanha e nenhum cliente notaria.
Toda API muda. A questao nao e se voce vai precisar de versionamento, mas como vai implementar.
| Abordagem | Exemplo | Prós | Contras |
|---|---|---|---|
| URL path | /v1/users | Simples, visivel, cacheavel | Duplica rotas, clientes hardcodam |
| Header | Accept: application/vnd.api+json;version=2 | Limpo, nao polui URL | Dificil de testar no browser, invisivel |
| Query param | /users?version=2 | Facil de testar | Poluicao semantica, cache complications |
Use URL path para versoes major e headers para negociacao fina.
# Versao major na URL
GET /v2/orders/ord_12345
# Formato especifico no header
Accept: application/json; version=2.3
A Stripe usa um modelo hibrido elegante: URL base com /v1/ e uma header Stripe-Version com data (2026-02-01). Quando voce cria uma conta, a versao da API e fixada naquela data. Mudancas futuras so afetam contas novas. Contas antigas continuam recebendo o comportamento da versao que conhecem.
Nao basta versionar. Voce precisa de uma politica de sunset — um contrato que diz quanto tempo versoes antigas serao mantidas.
# Header de resposta indicando deprecacao
Sunset: Sat, 01 Feb 2027 00:00:00 GMT
Deprecation: true
Link: <https://api.exemplo.com/docs/migration-v3>; rel="successor-version"
Boas praticas de sunset:
O GitHub e exemplar nisso. Quando deprecaram a API v3 em favor da GraphQL v4, mantiveram a v3 funcionando por anos, com documentacao clara de equivalencias.
A maioria das APIs retorna erros assim:
// O que a maioria faz
{
"error": "Bad Request"
}
// Ou pior
{
"error": true,
"message": "Validation failed"
}
Isso e inutil. O consumidor nao sabe o que errou, onde errou, nem como corrigir. E sao 3h da manha e o PagerDuty esta tocando.
// POST /v1/payments — 422 Unprocessable Entity
{
"error": {
"type": "validation_error",
"code": "INVALID_AMOUNT",
"message": "O valor do pagamento deve ser maior que R$ 0,50.",
"details": [
{
"field": "amount",
"value": 10,
"constraint": "minimum",
"minimum": 50,
"message": "O campo 'amount' deve ser >= 50 (centavos). Valor recebido: 10."
}
],
"request_id": "req_a1b2c3d4e5",
"documentation_url": "https://docs.exemplo.com/errors/INVALID_AMOUNT",
"timestamp": "2026-02-08T14:30:00Z"
}
}
Cada campo tem um proposito:
| Campo | Proposito |
|---|---|
type | Categoria do erro (machine-readable) para logica de retry/handling |
code | Codigo unico e estavel para esse erro especifico |
message | Descricao humana-legivel do que aconteceu |
details | Array com erros especificos por campo |
request_id | ID para correlacionar com logs do servidor (essencial para debug) |
documentation_url | Link direto para a doc desse erro |
timestamp | Quando o erro aconteceu (para debugging temporal) |
A Stripe opera consistentemente no nivel 3. Cada erro tem um codigo unico, uma mensagem clara e um link para documentacao. E isso nao e acidente — e design intencional que reduz tickets de suporte drasticamente.
200 OK — Sucesso
201 Created — Recurso criado com sucesso
204 No Content — Sucesso sem corpo (DELETE)
400 Bad Request — Erro de sintaxe/formato do request
401 Unauthorized — Autenticacao necessaria ou invalida
403 Forbidden — Autenticado mas sem permissao
404 Not Found — Recurso nao existe
409 Conflict — Conflito de estado (ex: pagamento ja processado)
422 Unprocessable — Dados validos sintaticamente mas semanticamente errados
429 Too Many Reqs — Rate limit excedido
500 Internal Error — Erro do servidor (nunca expor detalhes internos)
503 Unavailable — Servico indisponivel temporariamente
Regra: nunca retorne 200 com erro no body. Isso e crime contra a humanidade das APIs. O HTTP status code existe para isso — use-o.
APIs pequenas viram APIs grandes. O endpoint que retorna 50 registros hoje vai retornar 50.000 amanha. Se voce nao projetou paginacao desde o dia 1, vai sofrer.
Offset-based (simples, mas nao escala):
GET /v1/orders?offset=100&limit=25
Problema: com 1 milhao de registros, offset=999975 forca o banco a varrer quase 1 milhao de linhas. Performance degrada linearmente.
Cursor-based (escala infinitamente):
GET /v1/orders?cursor=eyJpZCI6MTIzNDV9&limit=25
A resposta com cursor-based pagination:
{
"data": [...],
"pagination": {
"has_more": true,
"next_cursor": "eyJpZCI6MTIzNzB9",
"previous_cursor": "eyJpZCI6MTIzNDV9"
},
"meta": {
"total_count": 15420,
"returned_count": 25,
"limit": 25
}
}
# Filtros simples (igualdade)
GET /v1/orders?status=processing&customer_id=cus_789
# Filtros com operadores
GET /v1/orders?created_at[gte]=2026-01-01&created_at[lt]=2026-02-01
GET /v1/orders?amount[gte]=5000&amount[lte]=50000
# Multiplos valores (OR)
GET /v1/orders?status[in]=processing,shipped
A Stripe usa exatamente esse padrao de brackets para operadores. E funciona ha anos sem mudancas.
# Ordenacao simples
GET /v1/orders?sort=created_at&order=desc
# Ordenacao multipla
GET /v1/orders?sort=status,-created_at
Convencao: prefixo - para descendente (padrao JSON:API).
Sempre envolva listas em um objeto. Nunca retorne um array nu no top-level.
// ERRADO - array nu
[
{ "id": "ord_001" },
{ "id": "ord_002" }
]
// CERTO - envelope com metadata
{
"data": [
{ "id": "ord_001" },
{ "id": "ord_002" }
],
"pagination": { ... },
"meta": { ... }
}
Por que? Porque se voce retorna um array nu, nao pode adicionar metadata de paginacao sem breaking change. O envelope te da espaco para evoluir a resposta.
Imagine: o cliente faz um POST /v1/payments para cobrar R$500. O request vai, o servidor processa, o pagamento e efetuado, mas a resposta se perde por timeout de rede. O cliente nao sabe se funcionou. Ele tenta de novo. O cliente foi cobrado R$1.000.
Isso e um problema real. E acontece com frequencia surpreendente.
POST /v1/payments
Idempotency-Key: idem_a1b2c3d4-e5f6-7890-abcd-ef1234567890
Content-Type: application/json
{
"amount": 50000,
"currency": "BRL",
"customer_id": "cus_789"
}
O servidor armazena o resultado associado a Idempotency-Key. Se receber o mesmo key novamente, retorna o resultado anterior sem processar de novo.
// Primeira chamada: processa e retorna 201
// Segunda chamada com mesmo key: retorna 200 com mesmo resultado
{
"id": "pay_xyz789",
"amount": 50000,
"currency": "BRL",
"status": "succeeded",
"idempotency_key": "idem_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"created_at": "2026-02-08T14:30:00Z"
}
| Metodo | Naturalmente Idempotente? | Acao Necessaria |
|---|---|---|
| GET | Sim | Nenhuma |
| PUT | Sim (deve ser) | Garantir que update e absoluto, nao incremental |
| DELETE | Sim (deve ser) | Retornar 204 mesmo se ja deletado |
| PATCH | Depende | Idempotency key recomendado |
| POST | Nao | Idempotency key obrigatorio |
# Pseudocodigo de middleware de idempotencia
def handle_request(request):
key = request.headers.get('Idempotency-Key')
if key:
cached = redis.get(f"idempotency:{key}")
if cached:
return cached # Retorna resultado anterior
# Processa o request normalmente
response = process(request)
if key:
# Armazena resultado por 24 horas
redis.setex(f"idempotency:{key}", 86400, response)
return response
A Stripe exige idempotency keys para toda operacao POST. Nao e opcional. E nao e por preciosismo tecnico — e porque eles processam bilhoes de dolares e uma cobranca duplicada e inaceitavel.
Idempotencia nao e feature. E higiene basica de API.
Toda API precisa de rate limiting. Sem ele, um unico cliente pode derrubar o servico para todos. Mas a forma como voce implementa rate limiting define se seus clientes vao te amar ou te odiar.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"error": "Rate limit exceeded"
}
O cliente sabe que foi limitado. Mas nao sabe:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1707400800
X-RateLimit-Policy: 1000;w=3600
Retry-After: 3600
{
"data": { ... }
}
| Header | Significado |
|---|---|
X-RateLimit-Limit | Total de requests permitidos no periodo |
X-RateLimit-Remaining | Quantos requests restam nesse periodo |
X-RateLimit-Reset | Timestamp Unix de quando o limite reseta |
X-RateLimit-Policy | Politica detalhada (1000 por hora) |
Retry-After | Segundos ate poder tentar de novo (no 429) |
// 429 Too Many Requests
{
"error": {
"type": "rate_limit_error",
"code": "RATE_LIMIT_EXCEEDED",
"message": "Limite de 1000 requests/hora excedido. Tente novamente em 847 segundos.",
"retry_after": 847,
"limit": 1000,
"period": "1h",
"current_usage": 1000,
"documentation_url": "https://docs.exemplo.com/rate-limits"
}
}
Por tier de cliente:
Free: 100 requests/hora
Starter: 1.000 requests/hora
Business: 10.000 requests/hora
Enterprise: 100.000 requests/hora (customizavel)
Por endpoint (protecao granular):
GET /v1/orders: 1000 req/hora
POST /v1/payments: 100 req/hora (operacao sensivel)
GET /v1/reports: 10 req/hora (query pesada)
Graceful Degradation: Em vez de cortar completamente em 429, considere retornar respostas degradadas — cache mais agressivo, dados menos frescos, mas sem bloquear totalmente.
O GitHub faz isso bem: a API v3 tem limites claros por tipo de autenticacao (60/hora sem auth, 5.000/hora com token), com headers em toda resposta, nao so no 429.
Este e o principio que une todos os outros. Se voce so pudesse levar um ensinamento deste artigo, seria este: nunca quebre contratos existentes.
Mudancas seguras (nao-breaking):
Mudancas perigosas (breaking):
Versao original (v1, 2024):
{
"id": "ord_123",
"customer_name": "Maria Silva",
"total": 15000
}
Evolucao segura (v1 atualizado, 2026):
{
"id": "ord_123",
"customer_name": "Maria Silva",
"customer": {
"id": "cus_789",
"name": "Maria Silva",
"email": "maria@exemplo.com"
},
"total": 15000,
"total_formatted": "R$ 150,00",
"currency": "BRL",
"metadata": {}
}
Observe: customer_name continua existindo mesmo apos adicionar o objeto customer. Parece redundante? E. Mas e backward compatible. Clientes antigos continuam funcionando. Clientes novos usam o objeto customer mais rico. Eventualmente, voce depreca customer_name com aviso — mas nao remove.
Em vez de sempre retornar objetos completos (que podem crescer demais), use expand:
# Resposta compacta (default)
GET /v1/orders/ord_123
{
"id": "ord_123",
"customer": "cus_789" // apenas ID
}
# Resposta expandida
GET /v1/orders/ord_123?expand[]=customer
{
"id": "ord_123",
"customer": {
"id": "cus_789",
"name": "Maria Silva",
"email": "maria@exemplo.com"
}
}
A Stripe usa expand extensivamente. Isso manteve a API leve para quem nao precisa de dados extras, e rica para quem precisa — sem nunca quebrar o contrato base.
Nao existe bala de prata. Cada protocolo resolve problemas diferentes e tem implicacoes diferentes para longevidade.
| Criterio | REST | GraphQL | gRPC |
|---|---|---|---|
| Melhor para | APIs publicas, integracao B2B | Apps com UI complexa, BFF | Microservicos internos, alta performance |
| Caching | Excelente (HTTP native) | Complexo (POST everywhere) | Possivel mas manual |
| Versionamento | URL/Header | Schema evolution | Proto backward compat |
| Curva de aprendizado | Baixa | Media | Alta |
| Documentacao | OpenAPI/Swagger | Schema introspection | Proto files |
| Longevidade provada | 20+ anos | ~10 anos | ~10 anos |
| Rate limiting | Trivial (por endpoint) | Complexo (por query cost) | Custom |
| Idempotencia | Padrao (HTTP methods) | Manual | Manual |
| Tipo de contrato | Implicito (docs) | Schema forte | Proto forte |
REST: quando voce tem uma API publica que precisa durar. Quando seus consumidores sao variados (mobile, web, terceiros, IoT). Quando caching HTTP importa. Quando simplicidade e uma feature. A maioria das APIs deveria ser REST.
GraphQL: quando voce controla o cliente e o servidor. Quando under-fetching e over-fetching sao problemas reais e mensurados (nao hipoteticos). Quando voce precisa de um BFF (Backend for Frontend) com queries flexiveis. Nao use GraphQL para APIs publicas a menos que voce seja do tamanho do GitHub.
gRPC: quando performance e critica e voce controla ambos os lados. Comunicacao interna entre microservicos. Streaming bidirecional. Quando voce precisa de tipagem forte com Protobuf. Nao exponha gRPC diretamente para clientes externos.
Internet → REST API (publica) → API Gateway → gRPC (interno entre servicos)
↓
GraphQL BFF (para apps proprios)
REST na borda. gRPC internamente. GraphQL como BFF se necessario. Essa arquitetura dura.
Se sua API nao tem uma spec OpenAPI, voce nao tem um contrato — voce tem uma descricao verbal de um contrato. E descricoes verbais sao ambiguas.
Code-First (o padrao): voce escreve o codigo da API e gera a spec depois. Problema: a spec vira documentacao desatualizada porque ninguem lembra de regerar.
Spec-First (o ideal): voce escreve a spec OpenAPI primeiro, valida com stakeholders, e depois gera codigo/stubs a partir dela. A spec e o contrato.
openapi: 3.1.0
info:
title: API de Pagamentos
version: "1.0"
description: API para processamento de pagamentos
contact:
email: api@exemplo.com
license:
name: Proprietaria
servers:
- url: https://api.exemplo.com/v1
description: Producao
- url: https://sandbox.api.exemplo.com/v1
description: Sandbox
paths:
/payments:
post:
operationId: createPayment
summary: Criar um novo pagamento
description: |
Cria um pagamento para um cliente.
Requer Idempotency-Key no header.
tags:
- Payments
parameters:
- name: Idempotency-Key
in: header
required: true
schema:
type: string
format: uuid
description: Chave unica para garantir idempotencia
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePaymentRequest'
example:
amount: 50000
currency: "BRL"
customer_id: "cus_789"
description: "Assinatura mensal"
responses:
'201':
description: Pagamento criado com sucesso
headers:
X-RateLimit-Limit:
schema:
type: integer
description: Limite de requests por hora
X-RateLimit-Remaining:
schema:
type: integer
description: Requests restantes no periodo
content:
application/json:
schema:
$ref: '#/components/schemas/Payment'
'422':
description: Erro de validacao
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Rate limit excedido
headers:
Retry-After:
schema:
type: integer
description: Segundos ate poder tentar novamente
components:
schemas:
CreatePaymentRequest:
type: object
required:
- amount
- currency
- customer_id
properties:
amount:
type: integer
minimum: 50
description: Valor em centavos (minimo R$ 0,50)
currency:
type: string
enum: [BRL, USD]
description: Moeda (ISO 4217)
customer_id:
type: string
pattern: '^cus_[a-zA-Z0-9]+$'
description: ID do cliente
description:
type: string
maxLength: 500
description: Descricao opcional do pagamento
metadata:
type: object
additionalProperties:
type: string
description: Metadados customizaveis (chave-valor)
Payment:
type: object
properties:
id:
type: string
description: ID unico do pagamento
example: "pay_xyz789"
amount:
type: integer
example: 50000
currency:
type: string
example: "BRL"
status:
type: string
enum: [pending, processing, succeeded, failed, refunded]
customer_id:
type: string
example: "cus_789"
created_at:
type: string
format: date-time
Error:
type: object
properties:
error:
type: object
properties:
type:
type: string
enum: [validation_error, authentication_error, rate_limit_error, api_error]
code:
type: string
message:
type: string
request_id:
type: string
documentation_url:
type: string
format: uri
Integre validacao de spec no CI/CD:
# Validar spec
npx @redocly/cli lint openapi.yaml
# Detectar breaking changes entre versoes
npx @openapitools/openapi-diff old-spec.yaml new-spec.yaml
# Gerar client SDK
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-axios \
-o ./sdk
O comando de diff e particularmente poderoso. Ele roda no CI e bloqueia o merge se detectar breaking changes. Isso transforma backward compatibility de "boa pratica" em "regra enforced por automacao."
Este e o topico que ninguem esta discutindo — mas que vai definir o design de API nos proximos anos.
Em 2026, LLMs e agentes de IA sao consumidores reais de APIs. Nao e teoria. Ferramentas como function calling do GPT, tool use do Claude e agentes autonomos estao fazendo requests HTTP a APIs reais. E eles tem exigencias diferentes de humanos.
1. Descricoes semanticas ricas
Um humano le "POST /payments" e entende. Um LLM precisa de mais contexto:
# Insuficiente para IA
summary: Criar pagamento
# Suficiente para IA
summary: Criar pagamento
description: |
Cria uma cobranca unica para um cliente existente.
O valor e em centavos (ex: 5000 = R$ 50,00).
Requer autenticacao via Bearer token.
Idempotente quando Idempotency-Key e fornecido.
Nao usar para assinaturas recorrentes (use /subscriptions).
2. Schemas tipados e validados
LLMs geram JSON. Quanto mais restritivo e bem documentado o schema, menos erros de geracao:
# Com constraints claros que um LLM pode seguir
amount:
type: integer
minimum: 50
maximum: 99999999
description: "Valor em centavos. Exemplo: 5000 para R$ 50,00"
3. Erros que explicam como corrigir
Quando um LLM recebe um erro, ele precisa entender o que fazer diferente. A mensagem de erro precisa ser correcao-oriented:
{
"error": {
"code": "INVALID_CURRENCY",
"message": "Moeda 'real' nao e valida. Use codigo ISO 4217: 'BRL' para Real Brasileiro.",
"valid_values": ["BRL", "USD", "EUR"],
"suggestion": "Substitua 'real' por 'BRL' no campo currency."
}
}
4. Exemplos completos na spec
LLMs aprendem melhor por exemplo do que por descricao abstrata. Cada endpoint deveria ter exemplos completos de request e response na spec OpenAPI.
Com function calling e tool use, sua spec OpenAPI esta se tornando, literalmente, a definicao de ferramenta que LLMs usam. Isso eleva drasticamente a importancia de:
createPayment > postV1PaymentsHandlerNa pratica, APIs bem projetadas segundo os principios deste artigo ja sao naturalmente "AI-friendly". Nomes claros, schemas tipados, erros ricos e documentacao completa beneficiam tanto humanos quanto maquinas.
A diferenca e que, com IA, esses principios deixam de ser "boas praticas opcionais" e se tornam requisitos funcionais. Uma API com descricoes vagas e erros opacos simplesmente nao funciona com agentes autonomos.
Inversao: em vez de mais principios positivos, vamos listar o que garante que sua API vai falhar. Se voce evitar esses 10 erros, ja esta a frente de 90% das APIs.
// O HORROR
HTTP/1.1 200 OK
{
"success": false,
"error": "Payment failed"
}
Use HTTP status codes. E para isso que eles existem. Clientes confiam no status code para logica de retry, circuit breakers e monitoring. Retornar 200 com erro no body sabota toda a cadeia.
GET /users/1
GET /users/2
GET /users/3 // facil de enumerar todos os usuarios
Use IDs opacos: usr_a1b2c3d4. Previne enumeracao, nao revela volume de dados e facilita migracoes de banco.
POST /api/createUser // ERRADO
POST /api/users // CERTO
GET /api/getOrderById/123 // ERRADO
GET /api/orders/123 // CERTO
POST /api/deleteUser/456 // CRIME
DELETE /api/users/456 // CERTO
Os metodos HTTP ja sao os verbos. Os endpoints devem ser substantivos (recursos).
{
"user_name": "Maria", // snake_case
"lastName": "Silva", // camelCase
"Email-Address": "m@x.com", // kebab-case
"PHONE": "11999999999" // SCREAMING_CASE
}
Escolha uma convencao e use em toda a API. JSON APIs geralmente usam snake_case (Stripe, GitHub) ou camelCase (Google). Misturar e sinal de falta de governanca.
// ERRADO - nao pode adicionar metadata sem breaking change
[
{ "id": 1 },
{ "id": 2 }
]
// CERTO - envelope expansivel
{
"data": [{ "id": 1 }, { "id": 2 }],
"meta": { "total": 2 }
}
{
"created": "02/08/2026" // Fevereiro 8 ou Agosto 2?
}
Sempre use ISO 8601 com timezone: "2026-02-08T14:30:00Z". Sem excecao.
Se voce tem rate limiting (e deveria ter), documente-o. Na spec, nos headers, na pagina de docs. Rate limiting sem documentacao e punir o usuario sem explicar o crime.
Nenhuma mudanca breaking deve acontecer sem minimo 90 dias de aviso, documentacao de migracao e suporte ativo. De preferencia, nenhuma mudanca breaking deveria acontecer nunca.
# Endpoint A usa header
Authorization: Bearer token123
# Endpoint B usa query param
/api/data?api_key=token123
# Endpoint C usa cookie
Cookie: session=token123
Escolha um metodo. Use em toda a API. Authorization: Bearer <token> para APIs modernas.
{
"id": "ord_123",
"status": "processing",
"links": {
"self": "/v1/orders/ord_123",
"cancel": "/v1/orders/ord_123/cancel",
"customer": "/v1/customers/cus_789",
"invoice": "/v1/invoices/inv_456"
}
}
Nao e sobre implementar HATEOAS completo (ninguem faz). E sobre fornecer links de navegacao uteis que ajudam o consumidor a descobrir acoes relacionadas sem ler toda a documentacao.
FUNDAMENTOS
[ ] IDs opacos com prefixo de tipo (ord_, cus_, pay_)
[ ] Nomenclatura consistente (snake_case OU camelCase, nunca ambos)
[ ] Datas em ISO 8601 com timezone (UTC)
[ ] Respostas em envelope (nunca arrays nus no top-level)
[ ] HTTP status codes corretos (nunca 200 com erro)
[ ] Recursos como substantivos, metodos HTTP como verbos
VERSIONAMENTO
[ ] Versao major na URL (/v1/, /v2/)
[ ] Politica de sunset documentada (minimo 12 meses)
[ ] Headers de deprecacao em endpoints legados
[ ] Migration guide para cada versao nova
[ ] Deteccao de breaking changes no CI/CD
ERROS
[ ] Estrutura consistente (type, code, message, details)
[ ] request_id em toda resposta de erro
[ ] documentation_url para cada tipo de erro
[ ] Mensagens actionable ("faca X" em vez de "erro Y")
[ ] Erros de validacao por campo com valor recebido
PAGINACAO E QUERIES
[ ] Cursor-based pagination como default
[ ] Filtros com sintaxe padronizada (brackets para operadores)
[ ] Sorting explicito com convencao de direcao
[ ] Limite maximo de resultados por pagina
[ ] Total count quando viavel (ou has_more flag)
IDEMPOTENCIA
[ ] Idempotency-Key obrigatorio para POST
[ ] PUT como operacao absoluta (nao incremental)
[ ] DELETE retorna 204 mesmo se recurso ja deletado
[ ] Cache de idempotencia com TTL definido (ex: 24h)
RATE LIMITING
[ ] Limites documentados por tier/endpoint
[ ] Headers X-RateLimit-* em TODA resposta
[ ] Retry-After no 429
[ ] Graceful degradation quando possivel
BACKWARDS COMPATIBILITY
[ ] Somente additive changes em versao existente
[ ] Campos novos sao opcionais
[ ] Campos antigos deprecados mas nao removidos
[ ] Testes de contrato no CI/CD
[ ] Expand pattern para dados opcionais
DOCUMENTACAO
[ ] OpenAPI spec como source of truth
[ ] Exemplos completos de request/response
[ ] Erros documentados com causa e correcao
[ ] Changelog publico
[ ] Sandbox/ambiente de teste disponivel
IA-READINESS
[ ] Descricoes semanticas ricas em cada operacao
[ ] Schemas com constraints explicitos (min, max, enum, pattern)
[ ] Exemplos em cada campo da spec
[ ] Nomes de operacao claros e descritivos
Se voce chegou ate aqui, voce ja sabe mais sobre design de API duravel do que a maioria dos devs com 10 anos de experiencia. Agora, a parte dificil: aplicar. Pegue o checklist acima, abra a API do seu projeto atual e faca uma auditoria honesta. Quantos items voce marca? Se for menos de 50%, voce tem trabalho pela frente — mas agora sabe exatamente o que fazer. Compartilhe este artigo com seu time. APIs duraveis nao sao construidas por individuos — sao construidas por equipes que concordam com os mesmos principios.