Rate Limiting por Rota: Proteção granular para endpoints críticos em Node.js

Imagine uma situação comum em APIs modernas: você tem um endpoint de login que, se desprotegido, pode sofrer tentativas de força bruta ilimitadas, enquanto outro endpoint que gera relatórios complexos consome recursos significativos do servidor. Tratar ambos com o mesmo limite seria como usar a mesma fechadura para proteger a porta da frente e o cofre da empresa – tecnicamente funciona, mas definitivamente não é a melhor estratégia.​

Este é o terceiro artigo da série sobre Rate Limiting na Prática, onde iremos explorar no 4 estratégias essenciais para proteger APIs desenvolvidas com Node.js. Nos artigos anteriores, apresentei os conceitos fundamentais do rate limiting e implementamos a proteção global, que aplica o mesmo limite uniformemente para todos os endpoints da aplicação.​ Se você não leu os artigos anteriores, fica aqui a recomendação de leitura.

Agora vamos evoluir nossa estratégia e aprender a implementar Rate Limiting por Rota, uma abordagem mais sofisticada que permite configurar limites específicos para cada endpoint, considerando suas características únicas de segurança e consumo de recursos.​

O que é Rate Limiting por rota?

O rate limiting por rota é uma técnica que permite aplicar diferentes configurações de limitação para endpoints específicos da sua API. Em vez de um limite único para toda a aplicação, você define regras personalizadas baseadas nas necessidades de cada rota.​

Pense em um shopping center com diferentes estabelecimentos: uma loja de eletrônicos tem um controle de entrada mais rigoroso que uma loja de roupas, e o banco dentro do shopping possui um nível de segurança ainda mais elevado. Cada estabelecimento define suas próprias regras de acesso, assim como cada endpoint pode ter seu próprio limite de requisições.​

Por que usar Rate Limiting por Rota?

Nem todos os endpoints de uma API são criados iguais. Alguns consomem mais recursos do servidor, outros são mais sensíveis a ataques, e muitos têm padrões de uso completamente diferentes.​

Um endpoint de autenticação como /auth/login é um alvo perfeito para ataques de força bruta, onde atacantes tentam adivinhar credenciais fazendo milhares de tentativas com diferentes combinações de usuário e senha. Para esse tipo de endpoint, você precisa de um limite muito restritivo – talvez apenas 5 tentativas por minuto.​

Por outro lado, endpoints que geram relatórios complexos ou processam grandes volumes de dados podem consumir recursos significativos do servidor. Mesmo que não sejam alvos diretos de ataques, permitir requisições ilimitadas pode sobrecarregar seu banco de dados ou CPU, degradando a performance para todos os usuários.​

Implementando Rate Limiting por Rota

Vamos implementar dois limitadores diferentes: um extremamente restritivo para o endpoint de login, e outro moderado para uma rota que gera relatórios pesados. O código utiliza a mesma biblioteca express-rate-limit que conhecemos na estratégia global, mas aplicada de forma direcionada.

Estrutura base da aplicação

Começamos importando as dependências necessárias e configurando o servidor Express:

import express from ‘express’;
import rateLimit from ‘express-rate-limit’;

const app = express();
const PORT = 3333;

app.use(express.json());
Note que, diferentemente da estratégia global, não aplicaremos nenhum limitador usando app.use() de forma geral. Cada limitador será aplicado especificamente nas rotas que precisam de proteção.​

Configurando o limitador para Login

O endpoint de login é um dos mais críticos em qualquer aplicação. Vamos criar um limitador altamente restritivo para protegê-lo contra tentativas de força bruta:

const loginLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minuto
  max: 5, // apenas 5 tentativas por IP
  message: {
    error: ‘Muitas tentativas de login’,
    message: ‘Tente novamente em 1 minuto’
  },
  skipSuccessfulRequests: true, // não conta logins bem-sucedidos
});

Cada parâmetro desta configuração foi cuidadosamente escolhido para equilibrar segurança e usabilidade.​

O windowMs está configurado para 60.000 milissegundos, ou seja, 1 minuto. Esta janela de tempo relativamente curta permite que usuários legítimos que erraram a senha tentem novamente rapidamente, mas dificulta significativamente ataques automatizados.​

O parâmetro max limita a apenas 5 requisições por minuto. Em teses, um usuário legítimo raramente precisa de mais de 5 tentativas de login em um minuto – se precisar, provavelmente esqueceu a senha e deveria usar a funcionalidade de recuperação.​ Este é um valor bastante restritivo, porém vale ressaltar que é um exemplo, você precisa entender o fluxo de requisições recebidas por sua API e ajustar esse limite.

A message personalizada retorna um objeto JSON informativo, deixando claro para o cliente que houve excesso de tentativas e quando poderá tentar novamente. Mensagens claras melhoram a experiência do usuário e facilitam a depuração durante o desenvolvimento, porém em alguns casos pode entregar informações preciosas para pessoas mal intencionadas que podem utilizá-las para ajustar os ataques em sua API. Por isso reflita bem qual mensagem será retornada.

A opção skipSuccessfulRequests é particularmente interessante para endpoints de autenticação. Quando configurada como true, ela instrui o rate limiter a não contabilizar requisições que retornaram sucesso (status HTTP menor que 400).​

Isso significa que se um usuário fizer uma tentativa de login válida, essa requisição não consome uma das 5 tentativas permitidas. O contador só é incrementado para tentativas falhadas – exatamente o comportamento que queremos para prevenir ataques de força bruta sem penalizar usuários legítimos.​

Configurando o limitador na emissão de relatórios

Endpoints que processam relatórios, executam consultas complexas ou manipulam grandes volumes de dados precisam de proteção diferente. O objetivo aqui não é prevenir tentativas maliciosas de acesso, mas evitar sobrecarga de recursos:

const heavyApiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minuto
  max: 10, // máximo 10 requisições por minuto
  message: {
    error: ‘Muitas tentativas deste IP’,
    message: ‘Tente novamente em 1 minuto’
  }
});

O windowMs mantém a mesma janela de 1 minuto do limitador de login, mas o max é configurado com um valor mais permissivo: 10 requisições por minuto.​

Diferentemente do loginLimiter, este limitador não utiliza skipSuccessfulRequests, pois queremos contabilizar todas as requisições independentemente do resultado. O objetivo é controlar o volume total de operações custosas, não apenas as que falharam.​

Aplicando os limitadores nas rotas

Agora vem a parte crucial: aplicar cada limitador especificamente nas rotas que precisam de proteção. No Express, isso é feito passando o middleware como segundo argumento na definição da rota:

app.post(’/auth/login’, loginLimiter, (_req, res) => {
  // res.status(404).json({ message: ‘Unauthorized’ });
  res.status(200).json({ message: ‘Authorized’ });
});

A rota /auth/login recebe o loginLimiter como segundo parâmetro, posicionado entre a definição do caminho e o handler da requisição. Isso garante que toda requisição POST para este endpoint passará primeiro pelo limitador antes de chegar à lógica de autenticação.​

O middleware intercepta a requisição, verifica o contador associado ao IP do cliente, e decide se permite ou bloqueia a chamada. Se bloqueada, o handler da rota nem chega a ser executado, economizando recursos computacionais.​

app.get(’/api/reports/sales’, heavyApiLimiter, (_req, res) => {
  res.json({ 
    message: ‘Relatório gerado’,
    data: ‘dados das vendas...’
  });
});

Da mesma forma, a rota /api/reports/sales utiliza o heavyApiLimiter. Apenas requisições GET para este endpoint específico serão limitadas por estas regras. Outras rotas da aplicação permanecem sem limitação, a menos que você configure limitadores específicos para elas.​

Está gostando desse conteúdo? Inscreva-se em nossa newsletter para receber conteúdos como esse diretamente no seu e-mail.

Iniciando o servidor

Por fim, colocamos o servidor para escutar na porta configurada:

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

Como funciona na prática

Quando uma requisição chega ao servidor, o Express processa os middlewares na ordem em que foram registrados. Para rotas com rate limiting por rota, a sequência é a seguinte:

  • O Express recebe a requisição e identifica que ela corresponde a uma rota que possui um limitador configurado.
  • O middleware do rate limiter é executado antes do handler da rota, verificando quantas requisições aquele IP já fez dentro da janela de tempo configurada.​
  • Se o limite não foi atingido, o contador é incrementado e a requisição prossegue para o handler da rota, que processa normalmente e retorna a resposta.
  • Se o limite foi atingido, o middleware bloqueia a requisição imediatamente, retorna o status HTTP 429 com a mensagem personalizada, e o handler da rota nem é executado.​

Comportamento do skipSuccessfulRequests

Para entender melhor o comportamento do skipSuccessfulRequests, imagine o seguinte cenário no endpoint de login:

  • Um usuário faz 4 tentativas de login com senha incorreta – o contador está em 4.
  • Na quinta tentativa, ele lembra a senha correta e faz login com sucesso.
  • Como skipSuccessfulRequests está configurado como true, essa requisição bem-sucedida não incrementa o contador.​

Tecnicamente, a requisição é contabilizada inicialmente e depois removida do contador quando o middleware detecta que a resposta teve sucesso (status HTTP menor que 400). Isso permite que usuários legítimos tenham chances ilimitadas de fazer login com sucesso, enquanto tentativas falhadas continuam limitadas.​

Testando a implementação

Para testar o comportamento diferenciado de cada limitador, você pode criar um arquivo de testes HTTP ou usar ferramentas como cURL, Postman ou Insomnia.

Testando o Endpoint de Login

Faça 5 requisições POST para /auth/login em sequência rápida. As 5 primeiras requisições serão processadas normalmente, retornando status 200. A sexta requisição, se feita dentro do mesmo minuto, será bloqueada com status 429 e a mensagem “Muitas tentativas de login”.​

Se você simular um login bem-sucedido alterando o código para retornar status 200, perceberá que essa requisição não consome uma das 5 tentativas permitidas, graças ao skipSuccessfulRequests.​

Testando o Endpoint de Relatórios

Para o endpoint /api/reports/sales, faça 10 requisições GET em sequência. As 10 primeiras serão processadas normalmente. A décima primeira requisição dentro do mesmo minuto será bloqueada com a mensagem personalizada.​

Note que o contador deste endpoint é independente do contador do endpoint de login. Você pode esgotar o limite de um endpoint sem afetar o outro, pois cada limitador mantém seu próprio controle por IP.​

Verificando os headers de Rate Limiting

Assim como na estratégia global, os limitadores por rota também retornam headers informativos nas respostas. Observe os headers RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset para acompanhar o estado de cada limitador.​

Vantagens da estratégia por rota

O rate limiting por rota oferece maior flexibilidade se comparado à estratégia global. Você pode calibrar precisamente os limites de cada endpoint baseado em suas características específicas, sem comprometer a usabilidade de rotas menos sensíveis.​

Endpoints críticos como autenticação, pagamentos ou alteração de dados sensíveis podem ter limites extremamente restritivos, enquanto endpoints de leitura simples mantêm limites mais permissivos. Essa abordagem maximiza a segurança onde necessário sem penalizar operações rotineiras.​

A otimização de recursos é outro benefício significativo. Ao limitar especificamente endpoints que consomem muitos recursos (como listagens, processamento de imagens ou operações complexas), você protege a infraestrutura contra sobrecarga mantendo outras operações fluidas.​

Cada limitador mantém seu contador independente, permitindo que usuários utilizem diferentes funcionalidades da API sem que o uso de uma afete a disponibilidade de outra. Isso resulta em uma experiência mais equilibrada e justa.​

Considerações importantes

Assim como a estratégia global, o rate limiting por rota usando express-rate-limit armazena os contadores em memória por padrão. Isso significa que cada instância da aplicação mantém seus próprios contadores, independentemente das outras.​

Para aplicações que rodam em múltiplas instâncias (escalamento horizontal), essa abordagem pode não ser ideal. Um cliente poderia teoricamente fazer 5 requisições em uma instância e outras 5 em outra instância, contornando parcialmente o limite. Para ambientes com múltiplas instâncias, a quarta estratégia desta série apresentará uma solução usando Redis para centralizar o controle.​

É fundamental monitorar e ajustar os limites ao longo do tempo. Valores muito restritivos frustram usuários legítimos, enquanto valores muito permissivos não protegem adequadamente. Use ferramentas de observabilidade para acompanhar quantos usuários estão atingindo os limites e ajuste conforme o padrão real de uso.​

Combinando com Rate Limiting Global

Uma estratégia comum em APIs de produção é combinar rate limiting global com limitadores por rota. A configuração global funciona como uma primeira camada de proteção aplicada a todos os endpoints, enquanto limitadores específicos adicionam proteção extra onde necessário.​

Por exemplo, você poderia configurar um limite global de 100 requisições por minuto para toda a API, e então adicionar um limitador específico de 5 requisições por minuto para o endpoint de login. Nesse cenário, o endpoint de login estaria duplamente protegido.​

Quando middlewares são combinados, o Express executa ambos na ordem em que foram registrados. Se o limitador global foi registrado com app.use() antes da definição das rotas, ele será executado primeiro, seguido pelo limitador específico da rota.​

Boas práticas de implementação

  • Sempre documente os limites na documentação da sua API: Desenvolvedores que consomem sua API precisam saber quais são os limites de cada endpoint para implementar lógica de retry e tratamento de erros apropriados.;
  • Configure mensagens de erro informativas que deixem claro qual foi o problema e quando o usuário poderá tentar novamente: Mensagens vagas como “Too many requests” são frustrantes – seja específico sobre o tempo de espera necessário;
  • Use skipSuccessfulRequests: true para endpoints de autenticação, mas tenha cuidado com outros tipos de endpoint: Para operações que consomem muitos recursos, você geralmente quer limitar todas as requisições, não apenas as que falharam;
  • Considere implementar limites diferentes por tipo de usuário se sua API possui planos pagos ou níveis de acesso: A opção keyGenerator do express-rate-limit permite usar identificadores além do IP, como tokens de autenticação, para aplicar limites personalizados.​

Próximos passos

O rate limiting por rota representa uma evolução significativa em relação à estratégia global, oferecendo controle granular e otimização de recursos. No próximo artigo desta série, exploraremos a terceira estratégia: Rate Limiting com headers customizados, onde aprenderemos a diferenciar entre clientes externos e serviços internos, permitindo que microsserviços se comuniquem livremente enquanto clientes externos permanecem limitados.​

Enquanto isso, experimente criar diferentes limitadores para diversos cenários de uso. Teste diferentes valores de windowMs e max, explore a opção skipSuccessfulRequests em diferentes contextos, e observe como cada configuração impacta o comportamento da sua API.​

O código completo desta implementação está disponível no repositório GitHub da série (que pode ser acessado aqui). Nos vemos no próximo artigo da série!​


Gostou do conteúdo? A proteção de APIs é fundamental para aplicações modernas, e dominar diferentes estratégias de rate limiting é uma habilidade essencial para desenvolvedores backend. Compartilhe este artigo com outros devs e continue acompanhando a série para aprender as demais estratégias práticas de proteção de APIs com Node.js.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *