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çã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.