Marcio Cunha

API lenta no NestJS? Como aplicar cache distribuído com Redis

Descubra como eliminar a latência de endpoints críticos no NestJS aplicando estratégias avançadas de cache distribuído e invalidação com Redis.

Marcio Cunha•4 min
Também disponível em:EnglishEspañol
Resumo
  • O armazenamento em cache reduz drasticamente o acesso direto ao banco de dados em endpoints de alta leitura
  • O Redis atua como uma camada rápida de memória compartilhada entre múltiplas instâncias de uma aplicação
  • A invalidação baseada em eventos evita dados desatualizados e mantém a consistência dos registros
  • A serialização eficiente de objetos otimiza o uso de memória e acelera as buscas no cluster
  • Interceptores personalizados no NestJS automatizam o ciclo de vida do cache sem poluir os controladores

Por que APIs lentas exigem uma estratégia de cache distribuído

Quando uma aplicação web cresce, o banco de dados costuma ser o primeiro componente a sofrer com a sobrecarga de consultas repetitivas. Se centenas de usuários pedem a mesma informação ao mesmo tempo, a API começa a demorar para responder e a experiência de uso desaba. Na prática, isso significa que o servidor perde tempo processando dados que não mudaram desde a última requisição. Para resolver esse problema de escala, precisamos de uma camada intermediária que guarde as respostas prontas e entregue os dados em frações de segundo.

Em arquiteturas modernas baseadas em microsserviços ou em múltiplos servidores rodando em paralelo, guardar dados apenas na memória RAM de uma única máquina deixa de funcionar bem. Se um usuário bate no servidor A, ele vê o dado atualizado, mas se a próxima requisição cai no servidor B, a informação pode estar desatualizada ou ausente. É exatamente aqui que entra o cache distribuído, uma memória compartilhada externa que todas as instâncias da aplicação conseguem consultar e atualizar de forma centralizada e síncrona.

O papel do Redis na aceleração de endpoints críticos no NestJS

O Redis é um banco de dados em memória extremamente rápido que funciona através de pares de chave e valor, ideal para armazenar dados temporários com altíssima performance. Ao integrá-lo com o NestJS, que é um framework modular e organizado para Node.js, conseguimos criar fluxos elegantes para interceptar requisições antes que elas cheguem às regras de negócio complexas. Na prática, o NestJS recebe a chamada HTTP, verifica se a chave correspondente já existe no Redis e, se estiver lá, devolve a resposta imediatamente sem consultar o PostgreSQL ou o MySQL.

Para conectar o Redis ao NestJS de forma limpa, utilizamos bibliotecas maduras como o ioredis combinadas com o módulo nativo de cache da plataforma. Isso permite configurar tempos de expiração automáticos, conhecidos como TTL ou Time to Live, garantindo que o dado expire sozinho após alguns minutos para evitar que informações antigas fiquem presas para sempre. Quando estruturamos essa camada com comunicação otimizada entre microsserviços, o ganho de desempenho em endpoints críticos é imediato e mensurável.

Como implementar interceptores de cache personalizados no NestJS

O NestJS facilita muito a manipulação do ciclo de vida das requisições através de interceptores, que funcionam como filtros inteligentes capazes de modificar a entrada e a saída de um endpoint. Podemos criar um interceptor dedicado que examina a rota chamada, gera uma chave única baseada nos parâmetros da URL e consulta o Redis antes mesmo do controller ser executado. Se o dado não existir, o interceptor deixa a requisição seguir, captura o resultado retornado pelo banco de dados, salva no Redis e só então entrega ao cliente.

Abaixo temos um exemplo prático de um interceptor em TypeScript que realiza essa verificação de forma transparente em qualquer rota onde for injetado:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import Redis from 'ioredis';

@Injectable()
export class RedisCacheInterceptor implements NestInterceptor {
  private redis = new Redis();

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = `cache:${request.url}`;

    const cachedData = await this.redis.get(cacheKey);
    if (cachedData) {
      return of(JSON.parse(cachedData));
    }

    return next.handle().pipe(
      tap(async (response) => {
        await this.redis.set(cacheKey, JSON.stringify(response), 'EX', 60);
      }),
    );
  }
}

Esse código reduz drasticamente o trabalho do servidor principal, mas traz um desafio clássico da engenharia desoftware: o que acontece quando o dado original é alterado no banco de dados?

Estratégias avançadas de invalidação para evitar dados fantasma

Guardar informações em cache é fácil, mas garantir que o usuário nunca veja um dado desatualizado após uma alteração é o verdadeiro teste de maturidade de um sistema. Quando um registro é atualizado ou excluído, a chave correspondente no Redis precisa ser imediatamente invalidada ou atualizada, caso contrário o sistema continuará servindo a versão antiga. Na prática, chamamos isso de invalidação baseada em eventos, onde cada operação de escrita no banco dispara um comando para apagar a chave obsoleta do Redis.

Uma abordagem robusta consiste em publicar eventos de mudança de estado através de um barramento interno ou mensageria, similar ao que ocorre quando estruturamos uma arquitetura resiliente com circuit breaker para proteger serviços dependentes. Dessa forma, desacoplamos a regra de negócio da limpeza do cache, garantindo que mesmo se o banco sofrer picos de escrita, a consistência dos dados retornados pela API permaneça intacta e confiável para os clientes. Para aprofundar esses conceitos na prática em produção, confira também nossa análise técnica sobre IA instável? Como usar DSPy para estruturar prompts.

Considerações finais sobre performance e escalabilidade

Aplicar cache distribuído com Redis no NestJS transforma a capacidade de atendimento de uma aplicação, reduzindo o tempo de resposta de centenas de milissegundos para poucos microssegundos. Contudo, essa facilidade exige disciplina na escolha das chaves, definição correta dos tempos de expiração e implementação rigorosa de rotinas de invalidação para evitar bugs difíceis de rastrear. Ao combinar essas práticas com um monitoramento constante do uso de memória, sua API estará pronta para escalar com segurança sob qualquer volume de tráfego.