Arquitetura de API REST: Boas Práticas para APIs em Produção
Como projetar APIs REST que escalam e são fáceis de manter: versionamento, autenticação, rate limiting, documentação e tratamento de erros.
APIs mal projetadas envelhecem mal
Uma API é um contrato. Clientes dependem dela, parceiros integram com ela, times internos a consomem. Uma API mal projetada acumula breaking changes, workarounds e frustração. Uma bem projetada sobrevive anos sem precisar de reescrita.
Design de recursos e endpoints
Substantivos, não verbos
# Errado: verbos na URL
POST /createUser
GET /getUser?id=123
POST /deleteUser
# Certo: substantivos + métodos HTTP
POST /users
GET /users/123
DELETE /users/123
Hierarquia de recursos com moderação
# Razoável
GET /organizations/42/members
POST /organizations/42/members
# Evitar hierarquias profundas
GET /organizations/42/teams/7/projects/3/tasks/99/comments/5
# Melhor:
GET /comments/5 (com filtros de contexto)
Filtros, paginação e ordenação
# Filtros como query params
GET /orders?status=pending&customer_id=42
# Paginação cursor-based (mais eficiente que offset)
GET /orders?cursor=eyJpZCI6MTAwfQ&limit=20
# Ordenação
GET /orders?sort=created_at&order=desc
Respostas consistentes
Defina um envelope de resposta e use-o em toda a API:
// Sucesso
{
"data": { "id": 1, "name": "Douglas", "email": "..." },
"meta": { "request_id": "abc-123" }
}
// Lista
{
"data": [...],
"pagination": {
"cursor": "eyJpZCI6MjB9",
"has_more": true,
"total": 450
}
}
// Erro
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Dados inválidos",
"details": [
{ "field": "email", "message": "E-mail inválido" }
]
}
}
Códigos HTTP corretos
| Situação | Código | |----------|--------| | Criou recurso | 201 Created | | Ação assíncrona aceita | 202 Accepted | | Sem conteúdo (DELETE) | 204 No Content | | Recurso não encontrado | 404 Not Found | | Não autorizado (sem auth) | 401 Unauthorized | | Sem permissão (auth ok) | 403 Forbidden | | Validação falhou | 422 Unprocessable Entity | | Rate limit excedido | 429 Too Many Requests |
Versionamento
# URL versioning (mais simples, mais explícito)
/v1/users
/v2/users
# Header versioning (URL mais limpa, menos observável)
Accept: application/vnd.api.v2+json
A regra: versione quando houver breaking change. Mantenha a versão anterior por pelo menos 6 meses após o anúncio de deprecation.
Autenticação e autorização
// Middleware de auth com JWT
export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: { code: 'UNAUTHORIZED' } })
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload
req.user = payload
next()
} catch {
return res.status(401).json({ error: { code: 'TOKEN_INVALID' } })
}
}
// Verificação de permissão por recurso
export function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user.permissions.includes(permission)) {
return res.status(403).json({ error: { code: 'FORBIDDEN' } })
}
next()
}
}
Rate Limiting
Proteger a API de abuso e DDoS:
import rateLimit from 'express-rate-limit'
// Limite geral
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: { code: 'RATE_LIMIT_EXCEEDED' } }
}))
// Limite mais restrito para autenticação
app.use('/auth', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
skipSuccessfulRequests: true // não conta tentativas bem-sucedidas
}))
Documentação com OpenAPI
openapi: 3.1.0
info:
title: API de Pedidos
version: 2.0.0
paths:
/orders:
get:
summary: Listar pedidos
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, processing, completed, cancelled]
responses:
'200':
description: Lista de pedidos
content:
application/json:
schema:
$ref: '#/components/schemas/OrderList'
Use swagger-jsdoc e swagger-ui-express para gerar a documentação automaticamente a partir dos comentários no código.
Idempotência para operações críticas
Pagamentos, envios de e-mail, criação de recursos — operações que não devem ser executadas duas vezes:
// Cliente envia Idempotency-Key: uuid-unico
// Servidor armazena resultado por 24h associado à chave
async function handlePayment(req: Request) {
const idempotencyKey = req.headers['idempotency-key']
if (idempotencyKey) {
const cached = await cache.get(`idempotency:${idempotencyKey}`)
if (cached) return JSON.parse(cached) // retorna resultado anterior
}
const result = await processPayment(req.body)
if (idempotencyKey) {
await cache.set(`idempotency:${idempotencyKey}`, JSON.stringify(result), 'EX', 86400)
}
return result
}
Conclusão
APIs bem projetadas são um investimento. O tempo gasto em consistência de resposta, versionamento cuidadoso, autenticação robusta e documentação atualizada retorna em velocidade de integração, menos suporte e longevidade do contrato. Projeta-se uma API esperando que ela dure anos.
