npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sabeesoft/idempotix-nestjs

v0.1.0

Published

NestJS module, decorator, and interceptor for idempotent HTTP endpoints.

Readme

@sabeesoft/idempotix-nestjs

NestJS integration for idempotix: one module registration and one decorator make an endpoint idempotent, with the idempotency record written inside your application's own database transaction.

Install

pnpm add @sabeesoft/idempotix-core @sabeesoft/idempotix-nestjs @sabeesoft/idempotix-prisma

Peer dependencies: @nestjs/common, @nestjs/core (11 or 12), reflect-metadata, rxjs. No dependency on any ORM or on nestjs-cls — those stay under your control.

Wiring with @nestjs-cls/transactional (recommended)

import { ClsModule } from 'nestjs-cls';
import { ClsPluginTransactional, TransactionHost } from '@nestjs-cls/transactional';
import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma';
import { IdempotixModule } from '@sabeesoft/idempotix-nestjs';
import { createPrismaIdempotencyStore } from '@sabeesoft/idempotix-prisma';

@Module({
  imports: [
    ClsModule.forRoot({
      middleware: { mount: true },
      plugins: [
        new ClsPluginTransactional({
          imports: [PrismaModule],
          adapter: new TransactionalAdapterPrisma({ prismaInjectionToken: PrismaService }),
        }),
      ],
    }),
    IdempotixModule.forRootAsync({
      inject: [TransactionHost],
      useFactory: (host: TransactionHost<TransactionalAdapterPrisma<PrismaClient>>) => ({
        store: createPrismaIdempotencyStore({ client: () => host.tx }),
        runInTransaction: (fn) => host.withTransaction(fn),
        ttl: '24h',
      }),
    }),
  ],
})
export class AppModule {}

runInTransaction wraps the guarded handler: withTransaction joins an already-active transaction or opens one, so the idempotency record and everything the handler writes through host.tx commit — or roll back — together. Interactive-transaction timeout/maxWait are configured on the adapter's defaultTxOptions.

Any other transaction manager works the same way: give runInTransaction a function that runs its argument inside a transaction and rethrows on failure, and give the store a client callback that returns the transaction's client.

Decorating a route

@Post()
@Idempotent()                      // or @Idempotent({ ttl: '1h', lockTtl: '30s', route: 'payments.create' })
createPayment(@Body() dto: CreatePaymentDto) { ... }

Behaviour for a request carrying an Idempotency-Key header:

| Situation | Response | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | First time this key is seen | handler runs; response stored | | Same key, same payload, completed | stored response replayed, Idempotent-Replayed: true | | Same key, different payload (params, query or body differ) | 422 Unprocessable Entity | | Same key, still processing | 409 Conflict with Retry-After (seconds) | | Handler throws | error propagates unchanged; the transaction rolls back and the key disappears, so the client can retry |

The key is scoped by tenant (see tenant below) and the route template (POST /payments/:id), never the resolved URL.

Options

| Option | Default | Notes | | ------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | store | — | required; e.g. createPrismaIdempotencyStore(...) | | ttl | '24h' | how long a completed response is replayable | | lockTtl | '60s' | how long an abandoned processing key blocks retries before it is reclaimed; keep it above your longest handler + transaction timeout | | keyHeader | 'Idempotency-Key' | header read case-insensitively | | keyExtractor | header lookup | (request) => string \| undefined; replaces the header lookup | | onMissingKey | 'generate' | 'generate' runs the request with a fresh random key (no protection against a retry — reported as the key_generated metric outcome); 'reject' answers 400 | | tenant | none | (request) => string \| null; isolates keys per client/tenant | | runInTransaction | pass-through | see above | | metrics | 'otel' | 'otel', 'noop', or a custom IdempotencyMetrics — see Metrics below | | clock | system clock | see @sabeesoft/idempotix-core |

Options are validated at startup; a bad value throws IdempotixConfigurationError naming the option.

Explicit mode: IdempotixService

For flows that own their transaction boundary, need several idempotent blocks in one request, or must keep a long-running external call outside the guarded section:

constructor(private readonly idempotix: IdempotixService) {}

async settle(key: string, order: Order) {
  const quote = await this.fxProvider.quote(order);          // outside any transaction

  const debit = await this.idempotix.run({
    key: `${key}:debit`,
    route: 'ledger.debit',                                   // low-cardinality operation name
    payload: { orderId: order.id, amount: order.amount },
    handler: () => this.ledger.debit(order, quote),
  });
  const credit = await this.idempotix.run({
    key: `${key}:credit`,
    route: 'ledger.credit',
    payload: { orderId: order.id, amount: order.amount },
    handler: () => this.ledger.credit(order, quote),
  });
  return { debit: debit.value, credit: credit.value };
}

run() returns { replayed, value } and throws FingerprintMismatchError / ConflictInProgressError from @sabeesoft/idempotix-core (map them yourself if they must become HTTP responses). It never opens a transaction: keep the acquire and the business write inside one transaction of your own (@Transactional() on the method, for instance). If you commit a processing record before completing it, a reclaim after lockTtl can race the original writer.

Metrics

Metrics are on by default and go through @opentelemetry/api only — until your app registers a MeterProvider, every instrument is a no-op. Meter name idempotix; units are seconds.

| Metric | Type | Attributes | | -------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | idempotix.requests | counter {request} | http.route, idempotix.outcome = acquired | reclaimed | replayed | conflict | fingerprint_mismatch | key_generated | | idempotix.processing | up-down counter {request} | http.route | | idempotix.handler.duration | histogram s | http.route, idempotix.result = success | error | | idempotix.transaction.duration | histogram s (only with runInTransaction) | http.route, idempotix.transaction.outcome = commit | rollback, error.type (the error's code, e.g. Prisma P2028 for an interactive-transaction timeout, or its class name) |

http.route is the route template (POST /payments/:id), never the resolved URL; no attribute can carry an idempotency key, request id or tenant. metrics: 'noop' disables them, a custom IdempotencyMetrics replaces them. For the database-side db.client.* metrics see @sabeesoft/idempotix-prisma's instrumentPrisma().

Minimal SDK bootstrap (run before Nest starts):

import { NodeSDK } from '@opentelemetry/sdk-node';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';

const sdk = new NodeSDK({
  serviceName: 'payments-api',
  metricReaders: [new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() })],
});
sdk.start();

Resource attributes (service.name, service.instance.id, …) come from your SDK setup; that is what lets a dashboard sum db.client.connection.count across services and replicas and compare it with the database's max_connections. The full catalogue, a Prometheus setup and dashboard queries are in docs/observability.md.

Known limitations

  • Replays re-use the route's static status (@HttpCode() or Nest's 201/200 default). A status set dynamically through @Res() is not replayed.
  • Only the HTTP context is intercepted; other transport contexts pass through untouched.
  • The stored response is the handler's return value serialised as JSON — the same thing the client received. Streams and non-JSON bodies are not supported. A replayed body is structurally identical to the original, but object key order may differ (PostgreSQL jsonb does not preserve it), so clients must not compare raw response bytes.