Como construir APIs RESTful escaláveis

1. Fundamentos de Escalabilidade em APIs RESTful

A escalabilidade em APIs RESTful começa com a compreensão dos princípios fundamentais do estilo arquitetural REST. O stateless é o pilar central: cada requisição do cliente deve conter todas as informações necessárias para o servidor processá-la, sem depender de estado armazenado no servidor entre requisições. Isso permite que qualquer servidor atenda qualquer requisição, facilitando o balanceamento de carga horizontal.

A cacheabilidade é outro princípio crítico. Respostas devem ser explicitamente marcadas como cacheáveis ou não-cacheáveis usando cabeçalhos HTTP, reduzindo drasticamente a carga no servidor para requisições repetidas.

Gargalos comuns incluem:
- Banco de dados: consultas N+1, falta de índices adequados
- Rede: payloads excessivos, latência de conexão
- Processamento serial: operações bloqueantes em fila única

Métricas essenciais para monitorar:
- Throughput: requisições por segundo (RPS)
- Latência p95/p99: tempo de resposta para 95%/99% das requisições
- Disponibilidade: percentual de tempo que a API responde corretamente

# Exemplo: Middleware de logging de métricas em Node.js
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms`);
    // Enviar métricas para sistema de monitoramento
    metrics.recordRequest(req.method, req.url, res.statusCode, duration);
  });
  next();
});

2. Design de Recursos e Endpoints Otimizados

A modelagem de recursos deve equilibrar granularidade e eficiência. Evite o problema N+1 queries onde uma lista de recursos dispara uma consulta adicional para cada item. Use eager loading e embedded resources quando apropriado.

Paginação é obrigatória para listas grandes. Use cursor-based pagination para dados em tempo real ou offset-based para dados estáveis.

# Exemplo: Endpoint com paginação e sparse fieldsets
GET /api/users?page=2&limit=20&fields=id,name,email&sort=created_at:desc

# Resposta paginada
{
  "data": [...],
  "meta": {
    "page": 2,
    "limit": 20,
    "total": 150,
    "next_cursor": "eyJpZCI6MTAwfQ=="
  }
}

Versionamento deve ser feito via cabeçalho Accept ou prefixo de URL (/v1/). Evite versionamento por query string, que polui a URL.

3. Estratégias de Cache e Redução de Carga

O cache HTTP é a primeira linha de defesa contra carga excessiva. Configure cabeçalhos adequados:

# Exemplo: Cabeçalhos de cache para recurso estável
Cache-Control: public, max-age=3600, s-maxage=7200
ETag: "686897696a7c876b7e"
Last-Modified: Tue, 15 Nov 2024 12:45:26 GMT

Para dados frequentemente acessados, implemente cache distribuído com Redis ou Memcached:

# Exemplo: Cache de consulta com Redis em Python
import redis
import json

cache = redis.Redis(host='redis-cluster', port=6379, decode_responses=True)

def get_user(user_id):
    cache_key = f"user:{user_id}"
    cached = cache.get(cache_key)
    if cached:
        return json.loads(cached)

    user = database.query("SELECT * FROM users WHERE id = %s", user_id)
    cache.setex(cache_key, 300, json.dumps(user))  # TTL de 5 minutos
    return user

CDN deve ser usada para assets estáticos e respostas GET com alta taxa de repetição. Edge computing (Cloudflare Workers, AWS Lambda@Edge) permite caching inteligente com lógica no edge.

4. Controle de Concorrência e Rate Limiting

Rate limiting protege contra abusos e picos de tráfego. Implemente por chave de API, IP ou usuário autenticado:

# Exemplo: Rate limiting com token bucket em Go
type RateLimiter struct {
    tokens    int
    maxTokens int
    refill    time.Duration
    mu        sync.Mutex
}

func (rl *RateLimiter) Allow() bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()
    if rl.tokens > 0 {
        rl.tokens--
        return true
    }
    return false
}

Para race conditions, use optimistic locking com tokens de concorrência:

# Exemplo: Atualização com optimistic locking
PATCH /api/items/123
Content-Type: application/json
If-Match: "abc123"

{
  "name": "Novo nome",
  "version": 2
}

Message queues (RabbitMQ, Kafka) absorvem picos de tráfego processando requisições de forma assíncrona. Implemente backpressure recusando requisições quando a fila atinge capacidade máxima.

5. Otimização de Banco de Dados e Consultas

Indexação estratégica é fundamental. Analise queries lentas com EXPLAIN e crie índices compostos para filtros comuns:

-- Índice composto para consultas frequentes
CREATE INDEX idx_users_status_created 
ON users (status, created_at DESC) 
WHERE status = 'active';

Para alta carga de leitura, use read replicas:

# Exemplo: Roteamento de leitura/escrita no Django
class DatabaseRouter:
    def db_for_read(self, model, **hints):
        return 'replica'
    def db_for_write(self, model, **hints):
        return 'primary'

Sharding horizontal distribui dados por múltiplos bancos usando chave de partição (ex: user_id % 10). Materialized views pré-calculam agregados complexos, reduzindo consultas pesadas.

6. Arquitetura de Microserviços e Balanceamento

Divida APIs monolíticas em serviços independentes com responsabilidades bem definidas. Cada serviço escala independentemente:

# Exemplo: Configuração de balanceamento com Nginx
upstream api_servers {
    least_conn;  # Algoritmo: menos conexões ativas
    server api1.example.com:8080 weight=3;
    server api2.example.com:8080 weight=2;
    server api3.example.com:8080 backup;
}

server {
    location /api/ {
        proxy_pass http://api_servers;
        health_check interval=5s;
    }
}

Service discovery com Consul ou etcd permite que serviços encontrem dinamicamente endpoints saudáveis. Health checks periódicos removem automaticamente instâncias com falha.

7. Monitoramento, Logging e Auto-scaling

Métricas chave para auto-scaling:
- Tempo de resposta: latência p95 > 500ms → escalar
- Taxa de erro: 5xx > 1% → escalar
- CPU/Memória: > 70% → escalar

Logging estruturado com JSON permite análise eficiente:

# Exemplo: Log estruturado com correlation ID
{
  "timestamp": "2024-11-15T10:30:00Z",
  "level": "ERROR",
  "service": "user-api",
  "trace_id": "abc123",
  "span_id": "def456",
  "message": "Database timeout",
  "duration_ms": 5000,
  "query": "SELECT * FROM users WHERE id = ?"
}

OpenTelemetry fornece tracing distribuído entre microserviços, permitindo identificar gargalos em cadeias de chamadas.

Auto-scaling horizontal com Kubernetes:

# Exemplo: HPA baseado em métricas customizadas
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-deployment
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Pods
    pods:
      metric:
        name: requests_per_second
      target:
        type: AverageValue
        averageValue: 1000

Referências