@mondart/nestjs-common-module-bottleneck
v3.2.10
Published
Bottleneck throttling and queue utilities for NestJS.
Readme
@mondart/nestjs-common-module-bottleneck
Per-key request throttling and queuing for NestJS, built on top of the
bottleneck library: a
BottleneckService for scheduling arbitrary work through a keyed limiter
group (optionally backed by Redis so limits are shared across instances), an
HTTP interceptor that throttles routes per resolved request key, and
CQRS bus wrappers that queue and deduplicate in-flight commands/queries.
Registration
import { BottleneckModule } from '@mondart/nestjs-common-module-bottleneck';
@Module({
imports: [
BottleneckModule.register({
appName: 'my-service',
basePath: 'orders',
requestKey: (req) =>
req.params?.orderId
? { key: 'orderId', value: req.params.orderId }
: null,
useGlobal: false, // routes must opt in via @UseBottleneckDecorator()
maxConcurrent: 1,
minTime: 0,
}),
],
})
export class AppModule {}Or asynchronously:
BottleneckModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
appName: config.get('APP_NAME'),
requestKey: (req) => ({ key: 'orderId', value: req.params.orderId }),
}),
});BottleneckModule is @Global(), so BottleneckService, QueuedCommandBus,
and QueuedQueryBus are available for injection anywhere without
re-importing the module. It also installs BottleneckInterceptor as a
global APP_INTERCEPTOR.
To share limits across instances instead of throttling each process
independently, set useRedis: true and provide redis: { host, port, ... }
— it's passed straight through to Bottleneck's ioredis datastore.
Throttling HTTP routes
BottleneckInterceptor runs on every HTTP request but only queues one that
opts in — nothing is throttled by default.
import {
UseBottleneckDecorator,
SkipBottleneckDecorator,
ConfigBottleneckDecorator,
} from '@mondart/nestjs-common-module-bottleneck';
@UseBottleneckDecorator()
@Patch(':orderId')
updateOrder() { ... }
@SkipBottleneckDecorator()
@Get(':orderId')
getOrder() { ... } // never throttled, even if useGlobal is set
@UseBottleneckDecorator()
@ConfigBottleneckDecorator({ maxConcurrent: 2, minTime: 500 })
@Post(':orderId/items')
addItem() { ... } // overrides the module-level limiter settings for this routeThe queue key is built from basePath and whatever requestKey(req)
resolves — e.g. orders/orderId:42, so all requests for the same order
serialize through the same limiter. If requestKey returns null/undefined
for a request that's otherwise opted in, the interceptor throws a
ServiceUnavailableException. If the per-key queue is already full
(maxQueueSize: 100), Bottleneck's rejection is also turned into a
ServiceUnavailableException instead of an unhandled error.
BottleneckService
Use this directly to throttle work that isn't an HTTP request (Kafka handlers, cron jobs, etc.).
constructor(private readonly bottleneckService: BottleneckService) {}
// Queue explicitly under a key, without in-flight caching:
await this.bottleneckService.scheduleForKey('order:42', () => doWork());
// Queue and, for the duration the task is pending, share the result with
// any other caller that uses the same key:
const result = await this.bottleneckService.runQueuedTask('order:42', () =>
fetchAndProcess(),
);Queued CQRS buses
QueuedCommandBus and QueuedQueryBus are drop-in replacements for
@nestjs/cqrs's CommandBus/QueryBus. Each execute() call is routed
through BottleneckService.runQueuedTask using a key derived from the
command/query's constructor name and its serialized fields, so two
identical commands dispatched concurrently share one execution instead of
running twice.
constructor(private readonly queuedCommandBus: QueuedCommandBus) {}
await this.queuedCommandBus.execute(new UpdateOrderCommand(orderId, dto));