Como usar caching de resposta com ETags e Cache-Control em APIs

1. Fundamentos do Caching em APIs REST

O caching de resposta é uma das técnicas mais eficazes para otimizar APIs REST. Quando implementado corretamente, reduz a latência das requisições, economiza banda de rede e diminui a carga no servidor. Em cenários de alta demanda, o caching pode reduzir o tempo de resposta de centenas de milissegundos para alguns poucos milissegundos.

Existem três níveis principais de caching:

  • Caching no cliente: o navegador ou aplicativo armazena respostas localmente
  • Caching em proxy reverso: servidores como Varnish, Nginx ou CDNs armazenam respostas intermediárias
  • Caching no servidor: o próprio servidor da API mantém um cache interno (ex.: Redis, Memcached)

Os cabeçalhos HTTP fundamentais para caching são:

  • Cache-Control: define diretivas de armazenamento e validade
  • ETag: identificador único da versão do recurso
  • Last-Modified: timestamp da última modificação
  • Expires: data de expiração (obsoleto em favor de max-age)

2. Configurando Cache-Control para Controle de Validade

O cabeçalho Cache-Control é o principal mecanismo para controlar o comportamento do cache. Suas diretivas mais importantes são:

Cache-Control: public, max-age=3600
Cache-Control: private, max-age=300
Cache-Control: no-cache, no-store, must-revalidate

Diretivas principais:

  • public: qualquer cache (incluindo proxies) pode armazenar a resposta
  • private: apenas o cache do cliente pode armazenar (navegador, não proxies)
  • no-cache: força revalidação com o servidor antes de usar cache
  • no-store: proíbe completamente o armazenamento em cache
  • max-age: tempo máximo em segundos que o recurso é considerado fresco
  • s-maxage: similar a max-age, mas aplica-se apenas a caches compartilhados (proxies/CDNs)

Estratégia para recursos estáticos vs. dinâmicos:

# Recurso estático (imagem, CSS, JS versionado)
Cache-Control: public, max-age=31536000, immutable

# Recurso dinâmico (lista de usuários)
Cache-Control: private, max-age=60

# Endpoint de autenticação
Cache-Control: no-store

Combinando com Vary:

O cabeçalho Vary informa que o cache deve considerar certos cabeçalhos para diferenciar versões:

Cache-Control: public, max-age=3600
Vary: Accept-Encoding, Authorization

Isso garante que versões comprimidas e não comprimidas do recurso sejam cacheadas separadamente, e que respostas para diferentes usuários não se misturem.

3. Implementando ETags para Validação de Recursos

ETags (Entity Tags) são identificadores únicos que representam o estado de um recurso. Quando o recurso muda, a ETag muda. O fluxo funciona assim:

  1. Servidor enha ETag: "abc123" na resposta
  2. Cliente armazena a ETag e, na próxima requisição, envia If-None-Match: "abc123"
  3. Servidor compara: se o recurso não mudou, retorna 304 Not Modified sem corpo
  4. Se mudou, retorna 200 OK com novo recurso e nova ETag

Gerando ETags:

# ETag forte (baseada em hash do conteúdo)
ETag: "d41d8cd98f00b204e9800998ecf8427e"

# ETag fraca (prefixo W/)
ETag: W/"1234567890"

# ETag baseada em timestamp + versão
ETag: "v2-1689023456"

Exemplo de fluxo completo:

Requisição inicial:

GET /api/users/123 HTTP/1.1
Host: api.exemplo.com

Resposta:

HTTP/1.1 200 OK
Content-Type: application/json
ETag: "a1b2c3d4"
Cache-Control: private, max-age=0, must-revalidate

{"id": 123, "nome": "João"}

Requisição subsequente:

GET /api/users/123 HTTP/1.1
Host: api.exemplo.com
If-None-Match: "a1b2c3d4"

Resposta (recurso inalterado):

HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4"
Cache-Control: private, max-age=0, must-revalidate

4. Estratégias Avançadas de Cache-Control para APIs

stale-while-revalidate e stale-if-error:

Permitem servir conteúdo obsoleto enquanto o cache é atualizado em segundo plano:

Cache-Control: public, max-age=3600, stale-while-revalidate=300, stale-if-error=86400
  • stale-while-revalidate=300: por 5 minutos após expirar, serve cache e revalida em background
  • stale-if-error=86400: por 24 horas, se o servidor falhar, serve cache obsoleto

Diretiva immutable:

Para recursos que nunca mudam (assets com hash no nome):

Cache-Control: public, max-age=31536000, immutable

Isso evita que o cliente sequer tente revalidar durante o período de validade.

APIs públicas vs. privadas:

# API pública (sem autenticação)
Cache-Control: public, max-age=300

# API privada (com token de autenticação)
Cache-Control: private, max-age=60
Vary: Authorization

5. Integração de ETags com Cache-Control na Prática

Implementação no servidor (Node.js/Express):

const crypto = require('crypto');

function cacheMiddleware(req, res, next) {
    // Configurar Cache-Control
    res.set('Cache-Control', 'private, max-age=0, must-revalidate');

    // Interceptar res.json para gerar ETag
    const originalJson = res.json.bind(res);
    res.json = function(body) {
        const content = JSON.stringify(body);
        const etag = crypto.createHash('md5').update(content).digest('hex');

        // Verificar If-None-Match
        const clientEtag = req.headers['if-none-match'];
        if (clientEtag && clientEtag === `"${etag}"`) {
            return res.status(304).end();
        }

        res.set('ETag', `"${etag}"`);
        return originalJson(body);
    };

    next();
}

app.get('/api/users/:id', cacheMiddleware, (req, res) => {
    const user = buscarUsuario(req.params.id);
    res.json(user);
});

Consumo no cliente (fetch):

let cachedEtag = null;

async function buscarUsuario(id) {
    const headers = {};
    if (cachedEtag) {
        headers['If-None-Match'] = cachedEtag;
    }

    const response = await fetch(`/api/users/${id}`, { headers });

    if (response.status === 304) {
        // Usar dados em cache local
        return dadosLocais;
    }

    const data = await response.json();
    cachedEtag = response.headers.get('ETag');
    return data;
}

ETags fracas vs. fortes:

  • ETag forte ("abc123"): representa exatamente o mesmo conteúdo byte a byte
  • ETag fraca (W/"abc123"): representa o mesmo significado semântico, mas não necessariamente idêntico byte a byte

Use ETags fracas quando o recurso pode ter diferenças irrelevantes (ex.: formatação de JSON, espaços em branco).

6. Cache em APIs com Dados Dinâmicos e Autenticação

Caching seguro para endpoints autenticados:

Cache-Control: private, max-age=60
Vary: Authorization

A diretiva private impede que proxies armazenem a resposta, e Vary: Authorization garante que cada token de autenticação tenha sua própria entrada no cache.

Caching de listas paginadas:

GET /api/users?page=2&limit=20&sort=nome

Cache-Control: public, max-age=120
Vary: Accept-Encoding

Para listas com muitos parâmetros de consulta, considere usar um cache baseado em URL completa no servidor.

Invalidando cache:

# Purge manual (se o cache suportar)
PURGE /api/users/123

# Versionamento de recursos
GET /api/v2/users/123

# TTLs curtos para dados voláteis
Cache-Control: public, max-age=30

7. Monitoramento e Depuração de Cache

Usando cURL para inspecionar cabeçalhos:

curl -I https://api.exemplo.com/users/123

# Resposta:
HTTP/2 200
cache-control: private, max-age=0, must-revalidate
etag: "a1b2c3d4"
age: 0

# Com If-None-Match:
curl -H "If-None-Match: \"a1b2c3d4\"" -I https://api.exemplo.com/users/123

# Resposta:
HTTP/2 304
etag: "a1b2c3d4"

Cabeçalho Age:

Indica há quantos segundos o recurso está em cache:

Age: 120

Logs de servidor:

[INFO] GET /api/users/123 - 304 (cache hit) - 2ms
[INFO] GET /api/products - 200 (cache miss) - 150ms

8. Boas Práticas e Armadilhas Comuns

Evitar cache de erros:

# Incorreto
Cache-Control: public, max-age=3600
HTTP/1.1 500 Internal Server Error

# Correto
Cache-Control: no-store
HTTP/1.1 500 Internal Server Error

Cuidado com Vary: *:

# Evite - invalida completamente o cache
Vary: *

# Prefira especificar apenas os cabeçalhos relevantes
Vary: Accept-Encoding, Authorization

Quando não usar ETags:

  • Recursos que mudam a cada requisição (ex.: timestamps, contadores)
  • Payloads muito grandes (a geração do hash consome CPU)
  • Endpoints que retornam dados diferentes para cada usuário (use private e no-cache)

Resumo de boas práticas:

  1. Sempre defina Cache-Control explicitamente
  2. Use ETag para revalidação eficiente
  3. Para recursos estáticos, use immutable com max-age longo
  4. Para APIs autenticadas, use private e Vary: Authorization
  5. Nunca cacheie respostas de erro sem no-store
  6. Monitore a taxa de cache hit/miss para ajustar TTLs

Referências