Como redigir documentos de API
Cada um dos três documentos precisa ter o suficiente para alguém construir, operar ou integrar sem uma reunião.
Você escreve documentação para alguém agir sem abrir o Slack. Quem precisa decidir, implementar ou operar depois da leitura define o documento.
No documento de projeto técnico você congela o que o time vai construir. Na documentação do pipeline você descreve como o dado se move, quebra e se recupera. Na especificação de API você amarra quem publica e quem consome: spec errada vira bug no código dos outros.
Misturar os três gêneros num wiki de 40 seções faz o time parar de abrir o arquivo.
Três documentos, três leitores
Nomeie o leitor antes da primeira linha. Sem nome, você ainda não escolheu o documento.
| Documento | Quem lê | O que essa pessoa precisa fazer depois |
|---|---|---|
| Projeto técnico | time que vai construir, quem aprova o risco, quem opera no go-live | decidir, implementar, recusar escopo |
| Pipeline de dados | engenheiro de dados, analista, on-call, auditor | operar, debugar, confiar no número |
| Especificação de API | time consumidor (interno ou externo), gerador de SDK, QA | integrar sem adivinhar |
RFC, OpenAPI e diagrama de Airflow respondem perguntas diferentes. Você congela a decisão no RFC e descreve o contrato HTTP no OpenAPI. O diagrama mostra o grafo do job; ainda falta escrever por que aquele job existe.
1. Documento de projeto técnico
Você vai ver o mesmo artefato como design doc, RFC ou TDD. Use o arquivo para congelar decisões que, se mudarem no meio da implementação, queimam uma sprint.
Escreva quando o trabalho atravessa mais de um serviço ou mais de um time, ou quando há dois caminhos possíveis. Um CRUD isolado no padrão do repositório não pede RFC. Um fluxo novo de pagamento ou uma migração de banco pedem.
Uma página para decidir
O leitor sênior precisa entender o problema e a decisão em dois minutos. O detalhe existe para quem for implementar.
Estrutura que funciona:
- Contexto: o que existe hoje e por que dói. Um parágrafo, sem história da empresa.
- Problema: a frase que, se estiver errada, todo o resto está errado.
- Não-objetivos: o que não entra. Corta discussão de escopo que voltaria no PR.
- Proposta: a solução escolhida, em prosa. Diagrama só se o texto deixar o fluxo ambíguo.
- Alternativas rejeitadas: uma linha cada, com o motivo. Sem isso, a mesma ideia volta no PR.
- Contratos: APIs, eventos, schemas, SLAs. Aponta para os docs específicos; não os duplica.
- Rollout e rollback: feature flag, migração, o que acontece se der errado na sexta.
- Riscos e dono: quem responde se produção discordar do desenho.
Exemplo de abertura que já decide:
# RFC-0142: Fila de webhooks de pagamento
**Status:** proposto
**Autor:** Ray
**Data:** 2026-09-07
## Problema
O endpoint `/webhooks/stripe` processa o evento no request.
Timeout do provedor + retry = cobrança duplicada.
## Não-objetivos
- Trocar de provedor de pagamento.
- Reprocessar eventos históricos anteriores a esta RFC.
- UI de conciliação. Isso é outro doc.
## Proposta
Ingestão síncrona só persiste o evento (idempotente por `event_id`).
Um worker consome a fila, aplica o efeito, grava `processed_at`.
Retry com backoff; poison queue depois de 8 tentativas.
Esse bloco já decide. Diagrama da fila, schema da tabela, métricas e runbook ficam para o implementador. O aprovador não precisa deles para aceitar.
Regras que evitam o PDF de 60 páginas
Decisão antes de implementação. Se o texto descreve como o código vai ficar e não o que você escolheu e por quê, você está antecipando o PR. Corte.
Um dono, um status. rascunho, em revisão, aceito, substituído. Sem status, o time trata o arquivo como opinião.
Números no lugar de adjetivos. Requisito: “p95 < 200ms no endpoint de checkout, região gru1”. “Baixa latência” não dá para testar.
Link, não copie. O schema do evento mora no spec da API ou no contrato do pipeline. O design doc aponta. Se você duplicar, na segunda semana os dois textos já divergiram.
Data de validade. Um RFC aceito em março sobre um serviço reescrito em agosto precisa de um aviso no topo: substituído por RFC-0198. Sem o aviso, alguém implementa o RFC morto.
2. Documentação de pipelines de dados
Se o pipeline só existe no DAG, só o autor consegue debugar. O documento é o contrato operacional do dado: origem e destino, atraso aceitável, o que fazer quando quebra.
Às 2h da manhã o on-call precisa saber se pode reprocessar o dia e quem acorda se o Slack disparar. Idempotência entra no mesmo bloco.
Anatomia de um pipeline documentado
Para cada pipeline (ou para cada dataset publicado), registre no mínimo:
| Campo | Por quê |
|---|---|
| Nome e dono | Slack/email de quem responde. Time, não “dados”. |
| Origem → destino | Sistema, tabela, tópico, bucket. Caminho completo. |
| Granularidade e janela | evento, snapshot diário, micro-batch de 5 min |
| Schema e evolução | campos, tipos, o que é breaking |
| Freshness / SLA | atraso máximo até o dado ser confiável |
| Idempotência | reprocessar o dia D duplica receita? |
| Dependências | jobs a montante, APIs, secrets |
| Falha | retry, dead-letter, alerta, runbook |
| Consumidores | quem quebra se este job parar |
Coloque um bloco YAML no repositório do pipeline, ao lado do DAG, versionado. Página no Notion some do radar em poucas sprints:
pipeline: billing.stripe_invoices_daily
owner: platform-data
oncall: @caw-data
schedule: "0 6 * * *" # 06:00 BRT
sla:
freshness: 8h
completeness: 99.5%
source:
system: stripe
object: invoice
cursor: created
destination:
warehouse: bigquery
dataset: billing
table: invoices
grain: invoice_id
idempotent: true
partition: invoice_date
schema_contract: contracts/billing.invoices.yaml
consumers:
- finance.mrr_dashboard
- saas.churn_model
on_failure:
retries: 3
backoff: exponential
alert: slack:#data-alerts
runbook: docs/runbooks/stripe-invoices.md
O contrato de schema fica noutro arquivo. O pipeline pode mudar de horário. O significado de mrr_cents fica no contrato.
# contracts/billing.invoices.yaml
dataset: billing.invoices
version: 3
breaking_change_policy: novo campo é aditivo; rename exige versão
fields:
- name: invoice_id
type: string
pk: true
- name: customer_id
type: string
required: true
- name: mrr_cents
type: int64
description: Recorrência mensal em centavos. Nunca em reais.
unit: cents
- name: status
type: enum
values: [draft, open, paid, void, uncollectible]
Deixe explícito
Linagem em uma frase. “Stripe invoice.paid → job stripe_invoices_daily → billing.invoices → dashboard MRR.” Se você não consegue escrever isso sem abrir três repos, o pipeline não está documentado.
Limitações conhecidas. billing.invoices não inclui trials. mrr_cents ignora add-ons manuais. Essa linha evita que o CFO tome decisão com o número errado.
Reprocessamento. Comando, janela máxima, efeito colateral. “Rode de novo o DAG” não é runbook se o job não for idempotente.
PII e retenção. Quais colunas são dado pessoal, quanto tempo ficam, se saem do Brasil. Em 2026, isso entra no contrato do pipeline. Jurídico não herda o bloco depois.
Teste de contrato no CI. Schema que só vive em markdown vai divergir. O YAML acima deve quebrar o build quando o job passar a emitir mrr em float.
O contrato descreve o dado. Trocar Airflow por Dagster deixa billing.invoices intacto.
3. Especificações de API
Sem spec, o consumidor acha o endpoint no Network tab. A especificação lista o que existe, como autentica, como falha e o que você promete manter estável.
A fonte da verdade é OpenAPI (3.1), versionada no repositório, gerada ou validada no CI. O README explica o motivo das decisões. O spec descreve operações, schemas e erros. Se os dois discordam, você corrige o README.
Spec usável
Trate a spec como contrato:
- Servidor e ambiente:
https://api.caw.agency/v1, não “o de prod”. - Auth: scheme, header, escopos. “Manda o token” não é spec.
- Recursos e operações: verbos reais, não um
POST /doSomethingpara tudo. - Schemas: request e response, required, enums, formatos (
date-time,email). - Erros: formato único, códigos estáveis, o que o cliente pode retryar.
- Idempotência: header
Idempotency-Keyem operações que cobram, criam ou disparam efeito. - Paginação e filtros: cursor vs offset, limites, ordenação default.
- Versionamento: o que é breaking, como convive
v1ev2. - Exemplos: um request real, um response real, um erro real.
Esboço mínimo (o arquivo completo mora em openapi.yaml):
openapi: 3.1.0
info:
title: CAW Billing API
version: 1.2.0
servers:
- url: https://api.caw.agency/v1
description: produção
security:
- bearerAuth: []
paths:
/invoices:
get:
operationId: listInvoices
summary: Lista faturas do workspace autenticado
parameters:
- $ref: "#/components/parameters/Cursor"
- name: status
in: query
schema:
$ref: "#/components/schemas/InvoiceStatus"
responses:
"200":
description: Página de faturas
content:
application/json:
schema:
$ref: "#/components/schemas/InvoicePage"
"401":
$ref: "#/components/responses/Unauthorized"
post:
operationId: createInvoice
summary: Cria fatura (idempotente)
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateInvoiceRequest"
responses:
"201":
description: Fatura criada
"409":
description: Idempotency-Key já usada com payload diferente
Esse trecho já substitui três páginas de documentação funcional. Schemas, exemplos e webhooks ficam no mesmo openapi.yaml.
Erros: um formato, sempre
Consumidor não deveria parsear HTML de nginx num caso e JSON noutro. Escolha um envelope e não saia dele.
{
"error": {
"code": "invoice_already_paid",
"message": "A fatura inv_8k2 já está paga.",
"retryable": false,
"docs": "https://docs.caw.agency/errors/invoice_already_paid"
}
}
code é estável e programável. message é para humano. retryable evita que o cliente invente a política de retry. HTTP status continua existindo (409, 422, 429); o body explica o domínio.
Guia de integração
OpenAPI não substitui um guia de integração. Duas páginas de prosa, no máximo:
- ordem das chamadas no fluxo feliz (criar cliente → criar fatura → registrar webhook);
- webhooks: eventos, assinatura, janela de retry, o que o receptor deve ignorar;
- sandbox vs produção;
- changelog de breaking changes.
Guia e spec são arquivos diferentes. Changelog de produto não mora no mesmo parágrafo que o schema de response.
Versionamento sem drama
- Breaking change (campo required novo, sentido de um enum, URL, auth) → nova versão (
/v2) ou header de versão explícito. - Campo aditivo, novo endpoint, novo valor de enum tolerado → minor, mesma URL.
- Nunca reaproveite o significado de um campo.
amountque era reais e passou a ser centavos é um bug disfarçado de melhoria. - Deprecie no spec (
deprecated: true) e no headerSunset/Deprecation. Sem os dois, o consumidor não sabe que a operação vai sumir.
Gere o spec a partir do código ou valide o código contra o spec no CI. Os dois caminhos fecham o ciclo. Atualizar o YAML “quando der” deixa o consumidor no escuro.
Regras dos três
Escreva no repositório. Markdown, YAML, OpenAPI. Review no PR. Na wiki da empresa o arquivo some do radar e o time deixa de revisar.
Presente, voz ativa, nomes reais. “O worker grava processed_at.” Não: “Deverá ser considerada a possibilidade de persistência do timestamp.”
Um conceito, um nome. Se no design doc é event_id e na API é webhookId, você já criou um ticket de suporte. Escolha um e propague.
Exemplos batem com o schema. Exemplo com amount: 10.50 e schema integer (centavos) é pior do que não ter exemplo.
Dono visível. CODEOWNERS no arquivo. Sem dono, em dois deploys o arquivo já descreve um sistema que não existe.
Envelheça de propósito. Data no topo. Status. Link para o sucessor. Sem isso o time lê um RFC morto como se ainda valesse.
Anti-padrões (e o que fazer no lugar)
Especificação por print do Figma. Tela não é contrato de API. Extraia os campos, os estados e os erros; o Figma ilustra.
Design doc que é a transcrição da call. Isso é áudio mal escrito. Extraia decisões e apague o resto.
Pipeline documentado só no README do Airflow. O DAG muda. O contrato do dataset deveria sobreviver à ferramenta.
“Ver código.” Código é a implementação atual. Spec é o que você promete não quebrar. Os dois existem porque divergem. O CI avisa quando isso acontece.
Documento único do projeto. Um Notion com arquitetura, dicionário de dados e lista de endpoints. O leitor não acha o contrato que precisa. Separe pelos três leitores da tabela do início.
Checklist antes de abrir o PR do doc
Projeto técnico
- Problema em uma frase; não-objetivos explícitos
- Alternativas rejeitadas com motivo
- Rollout, rollback e dono
- Links para specs de API e contratos de dado, sem duplicar schema
Pipeline
- Dono e canal de alerta
- Origem, destino, grain, SLA de freshness
- Schema versionado; política de breaking change
- Idempotência e comando de reprocessamento
- Limitações do número (o que o dado não mede)
API
- OpenAPI 3.1 no repo; exemplos de sucesso e erro
- Auth, idempotência, paginação
- Envelope de erro estável,
retryableexplícito - Política de versão e depreciação
- CI falha se implementação e spec divergirem
Item marcado para “depois do lançamento” deixa o documento incompleto. No go-live o on-call, o consumidor e o aprovador vão procurar essas linhas.
O atalho
Times rápidos escrevem o RFC de uma página antes do código, o contrato do pipeline antes do primeiro dashboard, o OpenAPI antes do SDK.
Na CAW Agency isso entra no fluxo briefing → código → evolução. Sem contrato, só o time original sabe ligar o sistema. A proposta em 24h cabe numa conversa e num arquivo; comitê não gera spec. Três arquivos certos cortam as semanas de alinhamento que vocês gastariam em call.
Quer colocar sua ideia no ar?
Conte o que você precisa e receba uma proposta em até 24 horas.