Skip to content

Remove all three runtime dependencies (zero-dependency middleware) #51

Description

@lawp09

Summary

Drop all three runtime dependencies (autobind-decorator, deep-equal, http-status-codes) in favor of native Node.js / TypeScript equivalents, making express-idempotency a zero-runtime-dependency middleware.

For a widely installed Express middleware this is a meaningful win: smaller install footprint, a smaller supply-chain surface, and fewer transitive breakages to inherit. The biggest supply-chain reduction comes from deep-equal (18 direct dependencies plus their subtree); the other two ship with 0 transitive dependencies.

Note: these replacements were validated by an adversarial review before scoping this issue. One of them (deep-equal) is not a safe naive swap — see the ⚠️ section under proposal 2. The plan below reflects the corrected approach.

Current usage

Dependency Where it's used What it actually does
http-status-codes src/errors/idempotencyErrors.ts, src/services/idempotencyService.ts Only two numeric constants: CONFLICT (409) and EXPECTATION_FAILED (417).
deep-equal src/defaults/defaultIntentValidator.ts Two calls comparing req.query and req.body against the original request (loose mode, no {strict:true}).
autobind-decorator src/services/idempotencyService.ts (@boundClass) Binds provideMiddlewareFunction, which is returned detached from its instance by the idempotency() factory.

Proposed replacements

1. http-status-codes → local constants — ✅ low risk

Replace HttpStatus.CONFLICT (409) and HttpStatus.EXPECTATION_FAILED (417) with local constants, ideally exported once from errors/idempotencyErrors.ts (already imported by the service) to avoid duplicated magic numbers.

  • Caveat: the test files still import http-status-codes, including a third constant HttpStatus.OK (200) at idempotencyService.test.ts:388. Either move http-status-codes to devDependencies (runtime goal still met) or refactor the tests onto local constants (and add OK = 200). Removing it from dependencies without one of these breaks npm test.

2. deep-equal⚠️ prototype-agnostic helper (NOT util.isDeepStrictEqual)

A naive swap to node:util.isDeepStrictEqual is a silent breaking change and must be avoided.

Why: the current call uses loose comparison, which does not compare prototypes. isDeepStrictEqual does. In Express 5 the default query parser is 'simple' (querystring.parse), so req.query is a null-prototype object (Object.create(null)). Any data adapter that serializes (Mongo, Redis, SQL — i.e. the production targets this library exists for) reads the stored query/body back as a plain Object.prototype object. At replay time:

isDeepStrictEqual(Object.create(null), {})        // → false  ❌  (even for an EMPTY query)
deepEqual(Object.create(null), {})                // → true   ✅  (current behavior)

Result: isDeepStrictEqual returns false for genuinely identical requests → the middleware raises 417 on every legitimate retry (idempotencyService.ts:136) → idempotency is broken in production. The existing test suite stays green (the InMemoryDataAdapter stores by reference and node-mocks-http uses Object.prototype), so the regression would ship undetected.

Correct approach:

  • Implement a small (~20 lines, zero-dep) prototype-agnostic deep-equal that compares keys/values like the current loose behavior, without inspecting [[Prototype]].
  • Add a regression test that round-trips the stored request through JSON.parse(JSON.stringify(...)) (simulating Mongo/Redis) before intent validation, running on Express 5 — otherwise this regression class reappears unnoticed.

3. autobind-decorator → explicit .bind() — ✅ low risk

@boundClass binds all public methods. Only provideMiddlewareFunction is actually returned detached (middleware/idempotency.ts:36), so a single bind makes the middleware work. However, to preserve the public contract (a consumer destructuring const { isHit } = getSharedIdempotencyService() works today), bind all three public methods in the constructor:

constructor(options: IdempotencyOptions = {}) {
  // ...
  this.provideMiddlewareFunction = this.provideMiddlewareFunction.bind(this);
  this.isHit = this.isHit.bind(this);
  this.reportError = this.reportError.bind(this);
}

The res.send monkey-patch (sendHook, ~idempotencyService.ts:378) and all internal callbacks are arrow functions capturing this lexically, so no extra binding is needed there.

4. Drop experimentalDecorators — ✅ depends on #3

@boundClass is the only decorator in the repo (no reflect-metadata, emitDecoratorMetadata already off). The flag lives once in tsconfig.json:63 and is inherited by tsconfig.dist.json / tsconfig.test.json / tsconfig.e2e.json via extends. Remove it after #3 lands (removing it while @boundClass is still present fails compilation) — ideally in the same PR.

Acceptance criteria

  • http-status-codes no longer a runtime dependency; 409/417 surfaced via shared local constants. Tests updated or http-status-codes moved to devDependencies.
  • deep-equal replaced by a prototype-agnostic helper (NOT a naive isDeepStrictEqual swap); @types/deep-equal removed from devDependencies.
  • Regression test: stored request round-tripped through JSON.parse(JSON.stringify(...)) before intent validation, on Express 5, proving identical requests (including empty query) still match.
  • autobind-decorator replaced by explicit .bind() of the three public methods (provideMiddlewareFunction, isHit, reportError); experimentalDecorators removed from tsconfig.json.
  • dependencies block in package.json is empty (zero runtime dependencies).
  • Unit + e2e tests pass.
  • CHANGELOG updated.
  • README dependency mentions (if any) updated in both the English and French sections.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions