@nathapp/nestjs-idempotency
v3.3.0
Published
HTTP idempotency-key interceptor for NestJS (Express + Fastify), zero runtime dependencies
Downloads
67
Readme
@nathapp/nestjs-idempotency
HTTP idempotency-key support for NestJS, working on both the Express and Fastify
adapters, with zero runtime dependencies (Node crypto only).
Design
- Opt-in per route — decorate a mutating handler with
@Idempotent(). Every other route is untouched (the interceptor is a no-op without the metadata). - Client-supplied key — the client sends a stable
Idempotency-Keyheader on a retryable request. The first successful (2xx) response is stored and replayed on any later retry with the same key. - In-flight protection — a duplicate that arrives while the first request is
still processing is rejected with 409 (an atomic
putIfAbsentrecords apendingstate on first sight). - Payload guard — the request body is fingerprinted; reusing the same key with a different body is rejected with 422.
- Key scope — the storage key is derived from
method + path + Idempotency-Key, so the same key value on different endpoints is independent. - Pluggable storage —
IIdempotencyStoreseam with an in-memory default (single-process only). Multi-instance deployments supply a shared store (e.g. RedisSET NX) via the seam.
Usage
import { IdempotencyModule, IdempotencyInterceptor, Idempotent } from '@nathapp/nestjs-idempotency';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
imports: [IdempotencyModule.register()],
providers: [{ provide: APP_INTERCEPTOR, useClass: IdempotencyInterceptor }],
})
export class AppModule {}
@Controller('orders')
export class OrdersController {
@Post()
@Idempotent()
create(@Body() dto: CreateOrderDto) {
// runs at most once per Idempotency-Key
}
}Options
| Option | Default | Meaning |
|--------|---------|---------|
| store | InMemoryIdempotencyStore | Storage backend (IIdempotencyStore). |
| headerName | Idempotency-Key | Request header carrying the key. |
| ttlMs | 86_400_000 (24h) | How long a stored response is replayable. |
| requireKey | false | When true, a decorated route without the header returns 400. |
| replayHeaders | ['content-type'] | Response headers stored and replayed. |
