Fundamentos REST
Os 6 Constraints de Fielding (2000)
| Constraint | Descrição |
|---|---|
| Client-Server | Separação de responsabilidades: UI e lógica de dados são independentes |
| Stateless | Cada requisição contém toda a informação necessária; servidor não guarda estado de sessão |
| Cacheable | Respostas devem indicar se podem ser cacheadas; reduz carga no servidor |
| Uniform Interface | Interface uniforme entre componentes (identificação de recursos, manipulação via representações, mensagens autodescritivas, HATEOAS) |
| Layered System | Cliente 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/42Recursos 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ível | Nome | Descrição |
|---|---|---|
| 0 | The Swamp of POX | HTTP como tunnel de transporte; um único endpoint; tudo é POST |
| 1 | Resources | URLs distintas por recurso; ainda usa só POST/GET |
| 2 | HTTP Verbs | Usa verbos HTTP corretamente (GET, POST, PUT, DELETE) + status codes |
| 3 | Hypermedia (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 FoundNí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étodo | Seguro | Idempotente | Body? | Uso |
|---|---|---|---|---|
| GET | Sim | Sim | Não | Leitura de recurso |
| HEAD | Sim | Sim | Não | Metadados sem body (validar cache) |
| OPTIONS | Sim | Sim | Não | CORS preflight, capacidades do endpoint |
| POST | Não | Não | Sim | Criação; ações não-idempotentes |
| PUT | Não | Sim | Sim | Substituição completa do recurso |
| PATCH | Não | Não* | Sim | Atualização parcial |
| DELETE | Não | Sim | Raro | Remoção |
*PATCH pode ser idempotente dependendo da implementação (ex:
SET field=valuevsINCREMENT 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.1Status Codes
Grupos
| Grupo | Significado |
|---|---|
| 1xx | Informacional (raramente usado em REST) |
| 2xx | Sucesso |
| 3xx | Redirecionamento |
| 4xx | Erro do cliente |
| 5xx | Erro do servidor |
2xx — Sucesso
| Código | Nome | Quando usar |
|---|---|---|
| 200 | OK | GET, PUT, PATCH com body de resposta |
| 201 | Created | POST que criou recurso; incluir Location header |
| 202 | Accepted | Operação assíncrona aceita para processamento |
| 204 | No Content | DELETE, 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ódigo | Nome | Quando usar |
|---|---|---|
| 301 | Moved Permanently | URL mudou permanentemente; atualizar bookmarks |
| 302 | Found | Redirecionamento temporário |
| 304 | Not Modified | Resposta não mudou desde o cache (ETag/Last-Modified) |
4xx — Erros do Cliente
| Código | Nome | Quando usar |
|---|---|---|
| 400 | Bad Request | Request malformado, JSON inválido |
| 401 | Unauthorized | Não autenticado (token ausente ou inválido) |
| 403 | Forbidden | Autenticado, mas sem permissão |
| 404 | Not Found | Recurso não existe |
| 405 | Method Not Allowed | Método HTTP não suportado pelo endpoint |
| 409 | Conflict | Estado conflitante (ex: e-mail duplicado) |
| 410 | Gone | Recurso existia e foi deletado permanentemente |
| 422 | Unprocessable Entity | Dados válidos sintaticamente mas inválidos semanticamente |
| 429 | Too Many Requests | Rate limit atingido |
5xx — Erros do Servidor
| Código | Nome | Quando usar |
|---|---|---|
| 500 | Internal Server Error | Erro inesperado no servidor |
| 502 | Bad Gateway | Upstream retornou resposta inválida |
| 503 | Service Unavailable | Servidor indisponível (manutenção, sobrecarga) |
| 504 | Gateway Timeout | Upstream 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ãoDesign 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 /createUserHierarquia 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árioLimite 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=42Filtros, 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=20Versionamento 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=2Recomendaçã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.9Response 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 503Cache-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 5minCORS 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: trueRate 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=20Variação de cursor baseada em valores de coluna indexada. Ideal para feeds cronológicos.
Link Headers (RFC 5988)
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ário | Estratégia |
|---|---|
| Admin panel, relatórios com pulo de página | Offset/Limit |
| Feed infinito, timeline | Cursor-based |
| Exportação de dados, replicação | Keyset |
| 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 | jqJWT 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_abc123Basic 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:ordersAuthorization 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_tokenQuando usar cada um:
| Cenário | Flow |
|---|---|
| Serviço para serviço (backend) | Client Credentials |
| App web com usuário (SPA) | Authorization Code + PKCE |
| App mobile | Authorization Code + PKCE |
| Script/CLI interno | Client Credentials ou Device Flow |
| Legado server-side | Authorization 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/jsonMedia Type Versioning
GET /users/42
Accept: application/vnd.mycompany.v2+jsonComo 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:
- Anunciar deprecação com data de sunset (mínimo 6 meses para APIs públicas)
- Adicionar headers
DeprecationeSunset - Monitorar uso da versão antiga nos logs
- Sunset: retornar
410 Goneapó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
| Campo | Tipo | Descrição |
|---|---|---|
type | URI | Tipo do erro (link para documentação) |
title | string | Descrição curta, legível por humanos |
status | integer | HTTP status code |
detail | string | Explicação específica desta ocorrência |
instance | URI | Identificador ú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-cacheETag 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 FailedVary Header
# CDN cria entradas de cache separadas por Accept-Language
Vary: Accept-Language
# Cache separado por método de compressão
Vary: Accept-EncodingRate Limiting
Estratégias
| Estratégia | Comportamento | Uso |
|---|---|---|
| Fixed Window | Conta resets em janela fixa (ex: por minuto) | Simples, pode ter burst no limite da janela |
| Sliding Window | Janela deslizante; distribui uniformemente | Mais justo, mais complexo |
| Token Bucket | Tokens acumulam com o tempo; burst até o bucket | Permite burst controlado; padrão em APIs de grande escala |
| Leaky Bucket | Processa a taxa constante; fila o excesso | Rate 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
| Vale | Não vale |
|---|---|
| API pública consumida por clientes desconhecidos | API interna consumida por um único frontend |
| Workflows complexos com estados variáveis | CRUD simples |
| Quando a URL pode mudar sem quebrar clientes | Clientes 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 FailedRequest 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-446655440000Permite 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 consumidoresOpenAPI 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'