Protocolos & APIs

REST API

Referência completa de REST — métodos HTTP, status codes, design de recursos, autenticação, versionamento, paginação, HATEOAS e boas práticas

Fundamentos REST

Os 6 Constraints de Fielding (2000)

ConstraintDescrição
Client-ServerSeparação de responsabilidades: UI e lógica de dados são independentes
StatelessCada requisição contém toda a informação necessária; servidor não guarda estado de sessão
CacheableRespostas devem indicar se podem ser cacheadas; reduz carga no servidor
Uniform InterfaceInterface uniforme entre componentes (identificação de recursos, manipulação via representações, mensagens autodescritivas, HATEOAS)
Layered SystemCliente não sabe se fala diretamente com o servidor ou com um intermediário (proxy, CDN, load balancer)
Code on Demand (opcional)Servidor pode enviar código executável ao cliente (ex: JavaScript)

REST vs RESTful vs HTTP API

  • REST: estilo arquitetural definido por Fielding; implica todos os 6 constraints
  • RESTful: API que segue os princípios REST (na prática, muitas usam só parte deles)
  • HTTP API: qualquer API sobre HTTP; não precisa seguir REST (ex: RPC over HTTP, SOAP)
  • A maioria do que chamamos de “REST” é na verdade HTTP API com convenções REST

Recursos vs Ações

# ERRADO — verbos na URL (estilo RPC)
POST /createUser
POST /getUsers
GET  /deleteUser?id=42

# CORRETO — substantivos, ações expressas pelo método HTTP
POST   /users
GET    /users
DELETE /users/42

Recursos representam entidades do domínio, não operações. Quando a ação não mapeia bem para CRUD, use sub-recursos ou aceite um substantivo de ação pontual (/orders/42/cancellation).


Modelo de Maturidade de Richardson (RMM)

O RMM (Richardson Maturity Model), popularizado por Martin Fowler em 2010, define 4 níveis que descrevem o quanto uma API aproveita o HTTP de forma correta. A maioria das “APIs REST” do mercado está no nível 2.

NívelNomeDescrição
0The Swamp of POXHTTP como tunnel de transporte; um único endpoint; tudo é POST
1ResourcesURLs distintas por recurso; ainda usa só POST/GET
2HTTP VerbsUsa verbos HTTP corretamente (GET, POST, PUT, DELETE) + status codes
3Hypermedia (HATEOAS)Respostas incluem links que guiam as próximas ações possíveis

Nível 0 — The Swamp of POX

Usa HTTP apenas como transporte. Toda a lógica fica no body (estilo SOAP ou XML-RPC).

POST /api HTTP/1.1
Content-Type: application/json

{ "action": "getUser", "id": 42 }
POST /api HTTP/1.1
Content-Type: application/json

{ "action": "deleteOrder", "orderId": "ord-99" }

Nível 1 — Resources

Cada recurso tem sua própria URL, mas ainda não usa os verbos HTTP corretamente.

POST /users/42          → busca usuário (deveria ser GET)
POST /orders/ord-99/delete  → deleta pedido (deveria ser DELETE)

Nível 2 — HTTP Verbs ✅ (padrão de mercado)

Usa verbos HTTP e status codes com semântica correta. É o que a maioria das APIs modernas pratica.

GET    /users/42        → 200 OK
POST   /users           → 201 Created + Location: /users/43
PUT    /users/42        → 200 OK
DELETE /orders/ord-99   → 204 No Content
GET    /users/999       → 404 Not Found

Nível 3 — Hypermedia / HATEOAS ✅ (REST “puro” de Fielding)

Respostas incluem links que descrevem quais ações o cliente pode tomar a seguir — o cliente não precisa conhecer as URLs de antemão, apenas o ponto de entrada.

GET /orders/ord-123

{
  "id": "ord-123",
  "status": "pending",
  "total": 199.90,
  "_links": {
    "self":    { "href": "/orders/ord-123" },
    "pay":     { "href": "/orders/ord-123/payment", "method": "POST" },
    "cancel":  { "href": "/orders/ord-123/cancellation", "method": "DELETE" },
    "items":   { "href": "/orders/ord-123/items" },
    "customer":{ "href": "/users/usr-42" }
  }
}

O cliente descobre que pode pagar (pay) ou cancelar (cancel) o pedido através dos links — sem hardcodar URLs ou conhecer a máquina de estados.

Na prática: o nível 3 é raro em APIs privadas (adiciona complexidade sem benefício claro quando o contrato já é conhecido). Faz mais sentido em APIs públicas exploráveis ou hipermídia para browsers.


Métodos HTTP

Tabela de Métodos

MétodoSeguroIdempotenteBody?Uso
GETSimSimNãoLeitura de recurso
HEADSimSimNãoMetadados sem body (validar cache)
OPTIONSSimSimNãoCORS preflight, capacidades do endpoint
POSTNãoNãoSimCriação; ações não-idempotentes
PUTNãoSimSimSubstituição completa do recurso
PATCHNãoNão*SimAtualização parcial
DELETENãoSimRaroRemoção

*PATCH pode ser idempotente dependendo da implementação (ex: SET field=value vs INCREMENT field).

Seguro = não modifica estado no servidor. Idempotente = N chamadas idênticas == 1 chamada.

PUT vs PATCH

# PUT — substitui o recurso inteiro; campos omitidos são removidos/nullados
PUT /users/42
Content-Type: application/json

{
  "name": "Rafael",
  "email": "rafael@example.com",
  "role": "admin"
}

# PATCH — atualização parcial; apenas os campos enviados são modificados
PATCH /users/42
Content-Type: application/json

{
  "email": "novo@example.com"
}

Use PUT quando o cliente envia a representação completa e conhecida do recurso. Use PATCH para atualizações pontuais — é o padrão mais comum em APIs modernas.

Exemplos Práticos

# GET com query params
GET /products?category=electronics&sort=price&order=asc HTTP/1.1
Host: api.example.com
Accept: application/json

# POST criando recurso
POST /orders HTTP/1.1
Content-Type: application/json

{"product_id": 10, "quantity": 2}

# DELETE idempotente — retorna 204 tanto na primeira quanto em chamadas subsequentes
DELETE /users/42 HTTP/1.1

Status Codes

Grupos

GrupoSignificado
1xxInformacional (raramente usado em REST)
2xxSucesso
3xxRedirecionamento
4xxErro do cliente
5xxErro do servidor

2xx — Sucesso

CódigoNomeQuando usar
200OKGET, PUT, PATCH com body de resposta
201CreatedPOST que criou recurso; incluir Location header
202AcceptedOperação assíncrona aceita para processamento
204No ContentDELETE, PUT/PATCH sem body de resposta
# 201 com Location
HTTP/1.1 201 Created
Location: /users/42
Content-Type: application/json

{"id": 42, "name": "Rafael"}

3xx — Redirecionamento

CódigoNomeQuando usar
301Moved PermanentlyURL mudou permanentemente; atualizar bookmarks
302FoundRedirecionamento temporário
304Not ModifiedResposta não mudou desde o cache (ETag/Last-Modified)

4xx — Erros do Cliente

CódigoNomeQuando usar
400Bad RequestRequest malformado, JSON inválido
401UnauthorizedNão autenticado (token ausente ou inválido)
403ForbiddenAutenticado, mas sem permissão
404Not FoundRecurso não existe
405Method Not AllowedMétodo HTTP não suportado pelo endpoint
409ConflictEstado conflitante (ex: e-mail duplicado)
410GoneRecurso existia e foi deletado permanentemente
422Unprocessable EntityDados válidos sintaticamente mas inválidos semanticamente
429Too Many RequestsRate limit atingido

5xx — Erros do Servidor

CódigoNomeQuando usar
500Internal Server ErrorErro inesperado no servidor
502Bad GatewayUpstream retornou resposta inválida
503Service UnavailableServidor indisponível (manutenção, sobrecarga)
504Gateway TimeoutUpstream não respondeu a tempo

200 vs 201 vs 204

POST /users → 201 Created  (criou recurso, retorna o recurso criado)
GET  /users → 200 OK       (leitura com body)
DELETE /users/42 → 204 No Content  (sem body de resposta)
PUT /users/42 → 200 OK (retorna recurso atualizado) ou 204 (sem body)

401 vs 403

401 Unauthorized → "Quem é você?" — token ausente, expirado ou inválido
403 Forbidden    → "Sei quem você é, mas não pode fazer isso" — falta de permissão

Design de URLs e Recursos

Naming — Substantivos no Plural

# Correto
GET  /users
GET  /users/42
GET  /users/42/orders
POST /users

# Errado
GET /getUser
GET /user/42       (singular inconsistente)
POST /createUser

Hierarquia de Recursos

/users                        → coleção de usuários
/users/{id}                   → usuário específico
/users/{id}/orders            → pedidos do usuário
/users/{id}/orders/{orderId}  → pedido específico do usuário

Limite a 2-3 níveis de aninhamento. Recursos muito aninhados ficam difíceis de manter.

Nested Routes vs Query Params

# Use nested routes para relacionamentos fortes (ownership)
GET /users/42/orders          → pedidos DO usuário 42

# Use query params para filtros, buscas e relacionamentos fracos
GET /orders?user_id=42        → mesma semântica, mais flexível para filtros compostos
GET /orders?status=pending&user_id=42

Filtros, Ordenação e Busca

# Filtros
GET /products?category=books&in_stock=true

# Ordenação (prefixo - para DESC)
GET /products?sort=price          # ASC
GET /products?sort=-price         # DESC
GET /products?sort=category,-price # múltiplos campos

# Busca full-text
GET /products?q=typescript

# Campos específicos (sparse fieldsets)
GET /users?fields=id,name,email

# Paginação junto com filtros
GET /orders?status=shipped&page=2&limit=20

Versionamento no Path vs Header vs Query Param

# URI versioning — mais explícito, mais fácil de testar no browser
GET /v1/users
GET /v2/users

# Header versioning — URL limpa, menos visível
GET /users
API-Version: 2

# Media type versioning — purista REST, mais complexo
Accept: application/vnd.myapi.v2+json

# Query param — não recomendado (quebra idempotência semântica)
GET /users?version=2

Recomendação pragmática: URI versioning para APIs públicas, header para APIs internas com controle do cliente.


Headers Importantes

Request Headers

Content-Type: application/json          # formato do body enviado
Accept: application/json                # formato esperado na resposta
Authorization: Bearer eyJhbGci...       # autenticação
If-None-Match: "abc123"                 # cache condicional com ETag
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT
If-Match: "abc123"                      # conditional PUT/DELETE (evita lost update)
X-Request-ID: 550e8400-e29b-41d4       # rastreabilidade (idempotency key)
Accept-Language: pt-BR, en;q=0.9

Response Headers

Content-Type: application/json; charset=utf-8
Location: /users/42                     # após 201 Created
ETag: "abc123"                          # versão do recurso para cache
Last-Modified: Tue, 15 Nov 2025 12:45:26 GMT
Cache-Control: max-age=3600, public
X-Request-ID: 550e8400-e29b-41d4       # echo do request ID
Retry-After: 60                         # após 429 ou 503

Cache-Control Directives

Cache-Control: no-store               # nunca cachear (dados sensíveis)
Cache-Control: no-cache               # cachear, mas sempre revalidar
Cache-Control: private, max-age=300   # cache só no browser, 5 min
Cache-Control: public, max-age=3600   # cache em CDN + browser, 1h
Cache-Control: public, s-maxage=3600, max-age=300  # CDN 1h, browser 5min

CORS Headers

# Preflight request (OPTIONS)
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

# Preflight response
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400          # cache do preflight por 24h
Access-Control-Allow-Credentials: true

Rate Limit Headers

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1735689600          # Unix timestamp do reset
Retry-After: 60                        # segundos para retry (após 429)

Paginação

Offset/Limit

GET /products?offset=40&limit=20

# Response
{
  "data": [...],
  "meta": {
    "total": 243,
    "offset": 40,
    "limit": 20,
    "has_next": true,
    "has_prev": true
  }
}

Prós: simples, fácil de pular para qualquer página. Contras: inconsistente com inserts/deletes concorrentes; lento em offsets grandes (full table scan até o offset).

Cursor-Based Pagination

GET /posts?cursor=eyJpZCI6MTAwfQ&limit=20

# Response
{
  "data": [...],
  "meta": {
    "next_cursor": "eyJpZCI6MTIwfQ",
    "prev_cursor": "eyJpZCI6MTAxfQ",
    "has_next": true
  }
}

O cursor geralmente é um base64 de {id: lastId} ou {created_at: "...", id: ...}. Prós: estável com inserts concorrentes; eficiente (índice, sem OFFSET). Contras: não permite pular páginas arbitrárias.

Keyset Pagination

GET /events?after_id=1234&limit=20
# ou com timestamp
GET /events?after=2025-11-01T00:00:00Z&limit=20

Variação de cursor baseada em valores de coluna indexada. Ideal para feeds cronológicos.

Link: </products?page=1&limit=20>; rel="first",
      </products?page=5&limit=20>; rel="last",
      </products?page=3&limit=20>; rel="next",
      </products?page=1&limit=20>; rel="prev"

Quando Usar Cada Estratégia

CenárioEstratégia
Admin panel, relatórios com pulo de páginaOffset/Limit
Feed infinito, timelineCursor-based
Exportação de dados, replicaçãoKeyset
Dataset pequeno (<1000 itens)Offset simples

Autenticação e Autorização

Bearer Token (JWT)

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
# Decode JWT (sem verificação — só para debug)
echo "eyJhbGci..." | cut -d'.' -f2 | base64 -d | jq

JWT payload típico:

{
  "sub": "user-42",
  "email": "rafael@example.com",
  "roles": ["admin"],
  "iat": 1735689600,
  "exp": 1735693200
}

API Key

# Header (preferível — não fica em logs de URL)
X-API-Key: sk_live_abc123...

# Query param (evitar em produção)
GET /data?api_key=sk_live_abc123

Basic Auth (legado)

Authorization: Basic cmFmYWVsOnNlbmhh
# Base64 de "user:password"

Use apenas sobre HTTPS. Preferir JWT ou API Key para novas APIs.

OAuth 2.0 Flows

Client Credentials — machine-to-machine (sem usuário)

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=app&client_secret=secret&scope=read:orders

Authorization Code + PKCE — aplicações com usuário (SPA, mobile)

1. App gera code_verifier e code_challenge (SHA-256)
2. Redireciona para /authorize?response_type=code&code_challenge=...
3. Usuário autentica, servidor retorna code
4. App troca code + code_verifier por access_token

Quando usar cada um:

CenárioFlow
Serviço para serviço (backend)Client Credentials
App web com usuário (SPA)Authorization Code + PKCE
App mobileAuthorization Code + PKCE
Script/CLI internoClient Credentials ou Device Flow
Legado server-sideAuthorization Code (sem PKCE)

Versionamento

URI Versioning (mais comum)

/v1/users      → versão estável atual
/v2/users      → nova versão com breaking changes
# Exemplo com breaking change
# v1 retorna campo "full_name"
GET /v1/users/42
{"id": 42, "full_name": "Rafael Marques"}

# v2 separa em "first_name" e "last_name"
GET /v2/users/42
{"id": 42, "first_name": "Rafael", "last_name": "Marques"}

Header Versioning

GET /users/42
API-Version: 2
Accept: application/json

Media Type Versioning

GET /users/42
Accept: application/vnd.mycompany.v2+json

Como Deprecar Versões

# Response da versão deprecada
Deprecation: true
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Link: </v2/users>; rel="successor-version"

Processo recomendado:

  1. Anunciar deprecação com data de sunset (mínimo 6 meses para APIs públicas)
  2. Adicionar headers Deprecation e Sunset
  3. Monitorar uso da versão antiga nos logs
  4. Sunset: retornar 410 Gone após a data

Tratamento de Erros

RFC 7807 — Problem Details

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Validation Failed",
  "status": 422,
  "detail": "One or more fields are invalid.",
  "instance": "/orders/attempt-1234",
  "errors": [
    {
      "field": "email",
      "code": "invalid_format",
      "message": "Must be a valid email address"
    },
    {
      "field": "quantity",
      "code": "min_value",
      "message": "Must be greater than 0",
      "rejected_value": -1
    }
  ]
}

Campos Obrigatórios

CampoTipoDescrição
typeURITipo do erro (link para documentação)
titlestringDescrição curta, legível por humanos
statusintegerHTTP status code
detailstringExplicação específica desta ocorrência
instanceURIIdentificador único desta ocorrência

Erros de Negócio vs Técnicos

// Erro de negócio (4xx) — cliente pode corrigir
{
  "type": "https://api.example.com/problems/insufficient-balance",
  "title": "Insufficient Balance",
  "status": 422,
  "detail": "Account balance (R$ 50.00) is less than requested amount (R$ 150.00)",
  "instance": "/transactions/txn-9876"
}

// Erro técnico (5xx) — não expor detalhes internos
{
  "type": "https://api.example.com/problems/internal-error",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "An unexpected error occurred. Use the instance ID to contact support.",
  "instance": "/requests/req-abc123"
}

Nunca exponha stack traces, queries SQL ou mensagens internas de exceção em respostas de produção.


Cache e Performance

Cache-Control em Detalhe

# Dados públicos estáticos (CDN + browser)
Cache-Control: public, max-age=86400, immutable

# Dados públicos atualizáveis (CDN revalida, browser cacheia curto)
Cache-Control: public, s-maxage=3600, max-age=300, stale-while-revalidate=60

# Dados privados do usuário (somente browser)
Cache-Control: private, max-age=300

# Dados sensíveis (nunca cachear)
Cache-Control: no-store

# Sempre revalidar antes de usar cache
Cache-Control: no-cache

ETag e Validação Condicional

# 1. Primeira requisição
GET /products/42 HTTP/1.1

HTTP/1.1 200 OK
ETag: "v3-abc123"
Cache-Control: max-age=300

# 2. Requisição condicional (após cache expirar)
GET /products/42 HTTP/1.1
If-None-Match: "v3-abc123"

# 3a. Recurso não mudou
HTTP/1.1 304 Not Modified

# 3b. Recurso mudou
HTTP/1.1 200 OK
ETag: "v4-def456"

ETag para Concorrência Otimista (Lost Update Problem)

# Ler recurso com ETag
GET /orders/99
ETag: "v1-xyz"

# Atualizar somente se ETag bater
PUT /orders/99
If-Match: "v1-xyz"

# Se outro cliente já atualizou
HTTP/1.1 412 Precondition Failed

Vary Header

# CDN cria entradas de cache separadas por Accept-Language
Vary: Accept-Language

# Cache separado por método de compressão
Vary: Accept-Encoding

Rate Limiting

Estratégias

EstratégiaComportamentoUso
Fixed WindowConta resets em janela fixa (ex: por minuto)Simples, pode ter burst no limite da janela
Sliding WindowJanela deslizante; distribui uniformementeMais justo, mais complexo
Token BucketTokens acumulam com o tempo; burst até o bucketPermite burst controlado; padrão em APIs de grande escala
Leaky BucketProcessa a taxa constante; fila o excessoRate de saída constante

Headers e Response

# Response normal com headers de rate limit
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1735689600

# Quando limite é atingido
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1735689600
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/rate-limit-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit of 1000 requests/hour exceeded. Retry after 60 seconds.",
  "instance": "/requests/req-timeout-001"
}

Retry com Backoff Exponencial

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch(url, options);
    if (res.status !== 429) return res;

    const retryAfter = res.headers.get("Retry-After");
    const delay = retryAfter
      ? parseInt(retryAfter) * 1000
      : Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);

    if (attempt < maxRetries) await new Promise(r => setTimeout(r, delay));
  }
  throw new Error("Max retries exceeded");
}

HATEOAS e Hypermedia

Conceito

HATEOAS (Hypermedia as the Engine of Application State) — responses incluem links para ações disponíveis, permitindo ao cliente navegar a API sem conhecimento prévio das URLs.

Formato HAL (Hypertext Application Language)

{
  "id": 42,
  "status": "pending",
  "total": 150.00,
  "_links": {
    "self":    { "href": "/orders/42" },
    "payment": { "href": "/orders/42/payment" },
    "cancel":  { "href": "/orders/42/cancellation", "method": "DELETE" },
    "customer":{ "href": "/users/7" }
  },
  "_embedded": {
    "items": [
      {
        "product_id": 10,
        "quantity": 2,
        "_links": { "product": { "href": "/products/10" } }
      }
    ]
  }
}

JSON:API

{
  "data": {
    "type": "orders",
    "id": "42",
    "attributes": { "status": "pending", "total": 150.00 },
    "relationships": {
      "customer": { "data": { "type": "users", "id": "7" } }
    },
    "links": { "self": "/orders/42" }
  }
}

Quando HATEOAS Vale a Pena

ValeNão vale
API pública consumida por clientes desconhecidosAPI interna consumida por um único frontend
Workflows complexos com estados variáveisCRUD simples
Quando a URL pode mudar sem quebrar clientesClientes gerados a partir de OpenAPI spec

Na prática: a maioria das APIs modernas implementa HATEOAS parcialmente (links de paginação, Location após criação) sem seguir HAL ou JSON:API completo.


Boas Práticas e Checklist

Idempotency Key para POST

POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{"amount": 100.00, "currency": "BRL"}

O servidor armazena o resultado associado à chave. Reenvios com a mesma chave retornam o resultado original sem reprocessar. Essencial para operações financeiras e side effects custosos.

Conditional Requests — Evitar Lost Update

# 1. Leia com ETag
GET /articles/5
ETag: "rev-7"

# 2. Atualize com If-Match
PUT /articles/5
If-Match: "rev-7"

# Se conflito (outro cliente editou):
HTTP/1.1 412 Precondition Failed

Request IDs para Rastreabilidade

# Cliente envia (ou servidor gera se ausente)
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

# Servidor retorna no response e loga
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

Permite correlacionar logs entre serviços em arquiteturas distribuídas.

Timeout e Retry

// Timeout explícito com AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

const res = await fetch("/api/orders", { signal: controller.signal });
clearTimeout(timeoutId);

Regras gerais:

  • Defina timeouts em todas as chamadas externas (nunca confie no default infinito)
  • Retry apenas em erros idempotentes (GET, DELETE, 429, 503) ou com idempotency key
  • Nunca faça retry automático em POST sem idempotency key

Checklist de API Production-Ready

Segurança
[ ] HTTPS obrigatório (HSTS header)
[ ] Autenticação em todos os endpoints sensíveis
[ ] Rate limiting implementado
[ ] Input validation antes de processar
[ ] Nunca expor stack traces em 5xx

Design
[ ] URLs com substantivos no plural
[ ] Métodos HTTP corretos para cada operação
[ ] Status codes semânticos (201 para criação, 204 para delete, etc.)
[ ] Formato de erro padronizado (RFC 7807)
[ ] Versionamento definido antes do primeiro release público

Performance
[ ] Cache-Control headers em todos os GETs públicos
[ ] ETag ou Last-Modified para recursos cacheáveis
[ ] Paginação em todos os endpoints de coleção
[ ] Campos desnecessários removidos dos responses (projection)

Operabilidade
[ ] X-Request-ID gerado ou propagado
[ ] Logging estruturado com request_id
[ ] Health check endpoint (/health ou /actuator/health)
[ ] OpenAPI/Swagger spec atualizada e publicada
[ ] Deprecation headers em versões antigas

Confiabilidade
[ ] Idempotency Key em POSTs com side effects
[ ] If-Match em PUTs críticos
[ ] Retry-After em 429 e 503
[ ] Documentação de SLA/rate limits para consumidores

OpenAPI Snippet Mínimo

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
paths:
  /orders:
    post:
      summary: Create order
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string, format: uuid }
          required: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        "201":
          description: Order created
          headers:
            Location:
              schema: { type: string }
        "422":
          description: Validation error
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetail'