Home / Articles / req-guard-lite: A Minimal, TypeScript-First Rate Limiter for Express

This article is published in English.

req-guard-lite: A Minimal, TypeScript-First Rate Limiter for Express

Learn how a lightweight, zero-dependency Express rate limiter works, from in-memory defaults to Redis scaling and custom key generators.

1291 words

Every Express application eventually reaches a point where it needs rate limiting.

Whether the goal is shielding login routes, cutting down on spam, or just guarding against unintentional overuse, throttling incoming traffic quickly turns from a nice-to-have into a requirement once an API is exposed publicly.

While searching for a rate-limiting solution that fit a common set of needs, it became clear that although many strong libraries already exist, a lot of projects really just want something small, easy to reason about, and simple to customize.

That gap is what led to building req-guard-lite.

Why Another Rate Limiter?

Most APIs don't need a full enterprise security suite from the very first day.

Often, all you want is to be able to write something like:

app.use(rateLimit({
  max: 100,
  windowMs: 15 * 60 * 1000
}));

...and get back to building the rest of the app.

The design goals for this package were:

  • Stay lightweight
  • Be easy to set up
  • Be built with TypeScript in mind from the start
  • Allow easy extension
  • Work for both small projects and large-scale systems

Introducing req-guard-lite

req-guard-lite is a compact piece of Express middleware built to shield your API from an overwhelming volume of requests.

By default it runs entirely in memory, but it's also capable of scaling out to distributed setups by plugging into Redis.

Its scope is intentionally narrow — it does one job well:

Track incoming requests and reject clients once they cross the limit you've set.

Features

Lightweight implementation Zero runtime dependencies in the core package Works as Express middleware Native TypeScript support Redis integration available Support for custom storage backends Support for custom key generators

Getting Started

Install it as a companion to Express.

npm install req-guard-lite express

Next, wire it into your app.

import express from 'express';
import { rateLimit } from 'req-guard-lite';

const app = express();

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100,
    message: 'Too many requests, please try again later.'
});

app.use(limiter);

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.listen(3000);

That's the entire setup.

Your API now blocks any client sending more than 100 requests inside a 15-minute window.

Default In-Memory Store

Out of the box, request counters live in memory.

Practically, that means:

  • No Redis required
  • No database required
  • No extra configuration needed
  • Ideal for local development
  • Solid choice for production on a single server

For a large share of applications, this is all the rate limiting you'll ever need.

Scaling with Redis

Once an app expands to run across several servers or containers, those instances need a shared view of request counts.

That's the role Redis plays here.

import Redis from "ioredis";
import { createRedisStore } from "req-guard-lite/redis";

const redis = new Redis();
const limiter = rateLimit({
    max: 100,
    windowMs: 15 * 60 * 1000,
    store: createRedisStore(redis, {
        max: 100,
        windowMs: 15 * 60 * 1000
    })
});

With this in place, every server instance reads and writes the same counters, so limits stay consistent no matter which node handles a given request.

Custom Key Generators

Rate limiting by IP address isn't always the right approach.

In some cases you'd rather key limits off of:

  • A user ID
  • An API key
  • A tenant identifier
  • An organization
  • A JWT subject claim
  • Or any other identifier that fits your model

To support that, req-guard-lite allows you to supply your own key generator function.

const limiter = rateLimit({
    max: 100,
    keyGenerator: (req) =>
        req.headers["x-api-key"] as string
});

Or, keyed to the logged-in user instead:

const limiter = rateLimit({
    max: 50,
    keyGenerator: (req) =>
        (req as any).user.id
});

The middleware itself is agnostic about what the key represents — it simply keeps a count against whatever identifier your function returns.

Bring Your Own Store

Extensibility was a core requirement from day one.

Rather than locking you into Redis, req-guard-lite exposes a straightforward RateLimitStore interface. If your infrastructure already relies on:

  • PostgreSQL
  • DynamoDB
  • Memcached
  • MongoDB
  • SQLite
  • Some other custom caching layer

you can plug it in by implementing that single interface.

class MyStore implements RateLimitStore {
    consume(key: string) {
        // your implementation
    }
}

This design keeps the package adaptable to nearly any backend you're already running.

One Important Production Tip

If your app sits behind:

  • Nginx
  • An AWS Load Balancer
  • Heroku
  • Cloudflare
  • Any reverse proxy

make sure to configure Express correctly:

app.set("trust proxy", 1);

Skip this step and Express will often treat every incoming request as if it originated from the proxy itself, which means all your users end up sharing a single rate limit bucket. It's a one-line fix, but it heads off a production headache that catches a lot of teams off guard.

How It Works

The internal flow is deliberately minimal:

  1. A request comes in.
  2. The middleware generates a key for it (the client IP, by default).
  3. The active store increments the counter tied to that key.
  4. Once the counter crosses the configured threshold, the middleware responds with HTTP 429 Too Many Requests.
  5. If the limit hasn't been reached, the request passes through untouched.

Because the store is pluggable, this same flow works whether you're backed by memory, Redis, or a custom implementation.

Why TypeScript?

The entire library is authored in TypeScript, which brings:

  • Strict typing throughout
  • Richer editor autocompletion
  • Simpler long-term maintenance
  • APIs that are harder to misuse

TypeScript users get complete type definitions out of the box, with no extra @types packages to install.

Roadmap

Development is ongoing, and a few features are already planned.

v0.4.0

  • Support for standard rate-limit response headers
X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset

These headers give client applications visibility into how many requests remain before they hit the ceiling.

v0.5.0

Configurable hooks that fire when a limit is exceeded, useful for things like:

  • Logging
  • Metrics collection
  • Alerting
  • Analytics
  • Sending data to external monitoring tools

Why It's Open Source

This project isn't a reaction to existing libraries falling short — there are already several excellent rate-limiting solutions in the Node ecosystem. req-guard-lite exists because the goal was a package that is:

  • Compact enough to read and understand in one sitting
  • Simple to extend
  • Designed TypeScript-first
  • Free of unnecessary complexity
  • Flexible enough to scale up with real production needs

Building it has also been a valuable exercise in publishing packages, designing APIs, writing tests, integrating with Redis, and shaping abstractions that are pleasant for other developers to use.

Closing Notes

Open-sourcing a project is one of the most effective ways to sharpen your engineering skills. Once other developers can install, use, and contribute to your code, you're forced to think past your own immediate use case — documentation, API design, testing, versioning, and backward compatibility all become real constraints you have to design around.

req-guard-lite began as a small middleware built to solve a personal need, but the hope is that it grows into a useful, lightweight, and extensible rate-limiting option for other Express developers. Feedback, feature suggestions, and contributions are all welcome.