Ratelock

Integrations

NestJS

NestJS

Rate limiting module for NestJS 10 and 11, powered by any RateLock engine.

  • Engine-agnostic: bring a @ratelock/local, @ratelock/redis or @ratelock/postgres limiter
  • Both adapters supported: Express and Fastify, tested on every change
  • Application-wide by default: registers through APP_GUARD; routes opt out with @SkipRateLimit()
  • Lazy initialization: pass a factory and the engine starts on the first guarded request
  • Dual-family headers: RateLimit-* (RFC 9331) and X-RateLimit-*, configurable

Installation

pnpm add @ratelock/nestjs @ratelock/redis

Quick start

import { Module } from '@nestjs/common'
import { RatelockModule } from '@ratelock/nestjs'
import { fixedWindow } from '@ratelock/redis'

@Module({
    imports: [
        RatelockModule.forRoot({
            // Lazy factory: nothing initializes until the first guarded request
            limiter: () => fixedWindow({ url: process.env.REDIS_URL!, limit: 100, windowMs: 60_000 }),
            limit: 100,
            keyGenerator: req => req.ip ?? 'anon',
        }),
    ],
})
export class AppModule {}

Async configuration with your config service:

RatelockModule.forRootAsync({
    imports: [ConfigModule],
    inject: [ConfigService],
    useFactory: async (config: ConfigService) => ({
        limiter: () =>
            fixedWindow({ url: config.get('REDIS_URL')!, limit: 100, windowMs: 60_000 }),
    }),
})

Already have an instance? Pass it directly:

const limiter = await fixedWindow({ limit: 100, windowMs: 60_000 })
RatelockModule.forRoot({ limiter })

Exempting routes

import { Get } from '@nestjs/common'
import { SkipRateLimit } from '@ratelock/nestjs'

@Get('/health')
@SkipRateLimit()
health() {
    return { status: 'up' }
}

The decorator works at method or controller level; the method wins over the controller.

Manual guard binding

Set globalGuard: false and attach the guard yourself:

import { UseGuards } from '@nestjs/common'
import { RatelockGuard } from '@ratelock/nestjs'

@UseGuards(RatelockGuard)
@Get('/limited')
limited() {
    return { ok: true }
}

Options

OptionTypeDefaultDescription
limiterLimiter | (() => Limiter | Promise<Limiter>)requiredAny RateLock limiter. A factory is memoized and invoked once, on the first guarded request.
keyGenerator(req) => string | Promise<string>req.ipIdentifier the request is counted against; falls back to a shared 'anonymous' bucket when unknown.
headers'both' | 'rfc' | 'legacy' | false'both'Header families attached to responses.
limitnumber(none)Quota, used only to emit the *Limit headers.
denyStatusCodenumber429Status of the denial exception.
messagestring'Too Many Requests'JSON body carried by the denial exception.
globalGuardbooleantrueBind through APP_GUARD.

Denials throw a NestJS HttpException carrying { error: message }, so existing exception filters shape them. Retry-After (seconds) accompanies denials whenever the result exposes a reset or refill time.

Response headers

FamilyHeaders
RFC 9331RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset
LegacyX-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
Denial onlyRetry-After

RateLimit-Reset is always in seconds. Token bucket results project floor(tokens) onto *Remaining and the refill time onto Reset.

Identifiers behind proxies

The default identifier reads request.ip, available on both adapters and honoring their trust-proxy settings (app.set('trust proxy') on Express, trustProxy on Fastify). Behind a load balancer without trust configuration, all clients share one bucket. Set trust correctly or provide a keyGenerator. Never read raw x-forwarded-for yourself: attackers forge it per-request to bypass limits entirely.

How is this guide?

Last updated on

On this page