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

@birtalanrobert/idempotency

v1.1.0

Published

Idempotency keys for mutating endpoints

Readme

@birtalanrobert/idempotency

Idempotency keys for mutating endpoints.

This is needed wherever clients double-submit: a guest double-taps and the order becomes real food, a retried attack command is unrecoverable, a doubled payment is a refund and an apology.

Using it in a NestJS application

import { IdempotencyModule } from '@birtalanrobert/idempotency';

@Module({
  imports: [
    // …config, logger, database…
    IdempotencyModule.forRootAsync({
      inject: [ConfigModule.token()],
      useFactory: (config: AppConfig) => ({ ttlMs: config.IDEMPOTENCY_TTL }),
    }),
  ],
})
export class AppModule {}

@Global(), and it registers its own interceptor — there is nothing to add to APP_INTERCEPTOR yourself.

Register idempotencyEntities and idempotencyMigrations with the database module.

Marking a route

@Post()
@Idempotent()
async create(@Body() body: CreateThingDto) { /* … */ }

The client sends Idempotency-Key. A repeat with the same key and the same body replays the first response; the same key with a different body is refused rather than replayed, because silently answering a question the caller did not ask hides their bug.

Put it on anything a client will retry and that has an effect outside the database — sending an email, charging a card, issuing a link. A create that times out in the network leaves the caller unable to tell whether it happened, and without a key the safe behaviour is also the one that does it twice.

The commit boundaries are the design, and they are not symmetrical

The claim commits immediately, in its own transaction. A concurrent duplicate must be able to see the claim — which it cannot do if the claim is sitting uncommitted inside the first request's transaction. Two simultaneous requests would then both proceed.

The completion commits with the work. If complete() ran in a separate transaction, a crash between the two would leave the work done and the key unfinished, and the retry would do the work twice — the exact failure this package exists to prevent.

const claim = await idempotency.begin(key, 'POST /orders', body);
if (claim.outcome === 'replay') return claim.body;

await runInTransaction(dataSource, async () => {
  const order = await createOrder(body);
  await idempotency.complete(claim.record, 201, order); // joins this transaction
});

Key reuse with a different payload is rejected

Replaying the first response would answer a question the client did not ask, and hide a bug in their code. They get a 422 naming the header and a idempotency_key_reused code.

Abandoned claims expire

A process that dies mid-request leaves an in_progress claim. Without a lock timeout that key is poisoned forever and the client can never retry. Default: five minutes, comfortably longer than any request should take.

Notes

  • Scope is part of the identity. Without it, a client reusing one key across two endpoints would get the first endpoint's response from the second.
  • The unique index uses COALESCE(tenant_id, …) because Postgres treats NULLs as distinct — a plain UNIQUE(tenant_id, scope, key) would let two platform-level requests claim the same key simultaneously.
  • The response is stored as text, not jsonb. It is an opaque blob to be replayed verbatim, never queried into, and text keeps SQL NULL unambiguously meaning "no response recorded".