Pular para o conteúdo
arquitetura

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.

Douglas M. Pereira4 min de leitura
api restarquiteturabackendboas práticasnodejs

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çãoCódigo
Criou recurso201 Created
Ação assíncrona aceita202 Accepted
Sem conteúdo (DELETE)204 No Content
Recurso não encontrado404 Not Found
Não autorizado (sem auth)401 Unauthorized
Sem permissão (auth ok)403 Forbidden
Validação falhou422 Unprocessable Entity
Rate limit excedido429 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.