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.

Como redigir documentos de API

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:

  1. Contexto: o que existe hoje e por que dói. Um parágrafo, sem história da empresa.
  2. Problema: a frase que, se estiver errada, todo o resto está errado.
  3. Não-objetivos: o que não entra. Corta discussão de escopo que voltaria no PR.
  4. Proposta: a solução escolhida, em prosa. Diagrama só se o texto deixar o fluxo ambíguo.
  5. Alternativas rejeitadas: uma linha cada, com o motivo. Sem isso, a mesma ideia volta no PR.
  6. Contratos: APIs, eventos, schemas, SLAs. Aponta para os docs específicos; não os duplica.
  7. Rollout e rollback: feature flag, migração, o que acontece se der errado na sexta.
  8. 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_dailybilling.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 /doSomething para 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-Key em 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 v1 e v2.
  • 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. amount que era reais e passou a ser centavos é um bug disfarçado de melhoria.
  • Deprecie no spec (deprecated: true) e no header Sunset / 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, retryable explí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.

Falar conosco