{"_id":"@avi00/express-idempotency","name":"@avi00/express-idempotency","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@avi00/express-idempotency","version":"1.0.0","description":"Express middleware to prevent duplicate request processing using idempotency keys","main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"./stores":{"import":{"types":"./dist/stores/index.d.mts","default":"./dist/stores/index.mjs"},"require":{"types":"./dist/stores/index.d.ts","default":"./dist/stores/index.js"}}},"scripts":{"build":"tsup","test":"vitest run","test:watch":"vitest","prepublishOnly":"npm run build"},"keywords":["express","idempotency","idempotent","middleware","cache","redis","lock"],"author":"","license":"MIT","peerDependencies":{"express":"^4.0.0 || ^5.0.0"},"peerDependenciesMeta":{"ioredis":{"optional":true}},"devDependencies":{"@types/express":"^4.17.21","@types/node":"^20.11.0","@types/supertest":"^6.0.2","express":"^4.19.2","ioredis":"^5.4.1","supertest":"^7.0.0","tsup":"^8.0.2","typescript":"^5.3.3","vitest":"^1.4.0"},"gitHead":"44c94068ea7a6ba84fd232cd4c75a51cee20258b","_id":"@avi00/express-idempotency@1.0.0","_nodeVersion":"26.0.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-csWBMZvuvF+3PitTMLo1/AJeQkEnietlPQN1nJ7kBVt7bd0thLnujLF3nPJ1XnLfhqGVxopgslJrYgJto6P4aQ==","shasum":"6a8999013a75669dc770c9c1db72377f4c6de391","tarball":"https://registry.npmjs.org/@avi00/express-idempotency/-/express-idempotency-1.0.0.tgz","fileCount":17,"unpackedSize":75694,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHuiaq3MIwYxniblElLJ+4Lx/MobTSjwK/glVFJKmetFAiBT3EVXvGLfmItAZwnUtgAgBD4tWuv7mxdckiXxk/CWew=="}]},"_npmUser":{"name":"avi00","email":"avinashopensource925@gmail.com"},"directories":{},"maintainers":[{"name":"avi00","email":"avinashopensource925@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/express-idempotency_1.0.0_1783060556350_0.640730453865475"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-03T06:35:56.181Z","1.0.0":"2026-07-03T06:35:56.485Z","modified":"2026-07-03T06:35:56.711Z"},"maintainers":[{"name":"avi00","email":"avinashopensource925@gmail.com"}],"description":"Express middleware to prevent duplicate request processing using idempotency keys","keywords":["express","idempotency","idempotent","middleware","cache","redis","lock"],"license":"MIT","readme":"# express-idempotency\n\nA lightweight, robust, and type-safe Express middleware to prevent duplicate request processing (e.g. double-charges on payments) using Idempotency Keys. \n\nBuilt with strict adherence to **SOLID**, **DRY**, and **KISS** principles. Features zero unnecessary dependencies.\n\n## Features\n\n- **Middleware Pipeline (Chain of Responsibility):** Drop-in Express middleware.\n- **Storage Backend (Strategy Pattern):** Easily swap storage engines. Comes out of the box with:\n  - `MemoryStore`: High-performance in-memory cache using JS `Map` and a custom Least Recently Used (LRU) eviction algorithm.\n  - `RedisStore`: Atomic locking and caching using Redis (compatible with `ioredis` or any Redis-like client).\n- **Response Capture (Decorator Pattern):** Intercepts `res.json()`, `res.send()`, and `res.end()` to capture response status, headers, and body automatically without modifying route code.\n- **Fingerprinting:** Prevent key collisions across routes/users by hashing the `Idempotency-Key` along with the HTTP method, route path, and an optional `userId`.\n- **Fault-Tolerant:** Safely deletes locks and releases keys on unhandled errors, client aborts, or server error statuses (5xx) so clients can safely retry.\n\n---\n\n## Installation\n\n```bash\nnpm install express-idempotency\n# If you are using RedisStore, install ioredis:\nnpm install ioredis\n```\n\n---\n\n## Usage\n\n### 1. In-Memory Store (LRU)\n\n```typescript\nimport express from 'express';\nimport { idempotency } from 'express-idempotency';\nimport { MemoryStore } from 'express-idempotency/stores';\n\nconst app = express();\n\napp.use(idempotency({\n  store: new MemoryStore({ maxSize: 1000 }),\n  ttlSeconds: 86400, // Cache for 24 hours\n  enforceHeader: false, // If true, throws 400 Bad Request if Idempotency-Key is missing\n}));\n\napp.post('/payments', (req, res) => {\n  res.status(201).json({ success: true, txnId: 'txn_12345' });\n});\n```\n\n### 2. Redis Store (Using Connection String)\n\nTo use Redis, configure your client using your connection string, and pass the client instance to `RedisStore`. This allows you to configure timeouts, TLS, connection pools, and reuse the Redis connection across your application:\n\n```typescript\nimport express from 'express';\nimport { idempotency } from 'express-idempotency';\nimport { RedisStore } from 'express-idempotency/stores';\nimport Redis from 'ioredis';\n\nconst app = express();\n\n// Initialize ioredis with your Redis connection string (URI)\nconst redisClient = new Redis('redis://:your_password@localhost:6379/0');\n\napp.use(idempotency({\n  store: new RedisStore(redisClient, { keyPrefix: 'my-app:' }),\n  ttlSeconds: 86400, // Cache for 24 hours\n  // Tie idempotency keys to user accounts to prevent key collisions across users:\n  getUserId: (req) => req.headers['x-user-id'] as string | undefined,\n}));\n\napp.post('/orders', (req, res) => {\n  res.json({ orderId: 'ord_98765', status: 'created' });\n});\n```\n\n---\n\n## API Reference\n\n### `idempotency(options)`\n\nMiddleware creator. Takes the following options:\n\n| Option | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `store` | `IIdempotencyStore` | *Required* | Storage strategy to use (`MemoryStore` or `RedisStore`). |\n| `ttlSeconds` | `number` | `86400` (24h) | Time-to-live in seconds for cached responses and locks. |\n| `headerName` | `string` | `'Idempotency-Key'` | Header to inspect for the idempotency key. |\n| `enforceHeader` | `boolean` | `false` | If `true`, returns `400 Bad Request` if the header is missing. |\n| `getUserId` | `(req) => string \\| Promise<string>` | `undefined` | Extracts user ID to lock request scope to a specific user. |\n| `shouldCache` | `(res) => boolean` | `status < 500` | Predicate checking if a status code should be cached (5xx is not cached by default). |\n| `errorMessage` | `string` | `'Request in progress'` | Custom message returned for concurrent conflict requests (409). |\n\n### Custom Storage Strategy\n\nYou can implement your own strategy by implementing the `IIdempotencyStore` interface:\n\n```typescript\nimport { IIdempotencyStore, IdempotencyRecord } from 'express-idempotency';\n\nclass CustomStore implements IIdempotencyStore {\n  async get(key: string): Promise<IdempotencyRecord | null> {\n    // get from DB\n  }\n  async set(key: string, record: IdempotencyRecord, ttlSeconds: number): Promise<void> {\n    // save to DB\n  }\n  async setLock(key: string, ttlSeconds: number): Promise<boolean> {\n    // atomically set lock (status: 'started') and return true if key did not exist\n  }\n  async delete(key: string): Promise<void> {\n    // delete key\n  }\n}\n```\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-3a57eac2f93475cacf43bdc013d73fc5"}