DEV Community

Nakshatra Garg
Nakshatra Garg

Posted on

I built an npm package that catches API contract drift before your users do

You've seen this before.

Backend quietly renames user_id to userId. Or changes amount from a number to a string. Or drops a field entirely. The TypeScript types go stale. Nobody updates the frontend. And three days later, a user files a bug report about a blank screen.

This happens in every team, at every scale. I've personally shipped three production bugs in fintech because of it — and in fintech, a broken loan amount field is not a "minor UI glitch."

So I built something to stop it.


Introducing @nakshatra6350/api-diff

A zero-config fetch interceptor that watches every API response at runtime and tells you the moment something drifts from what you expected.

npm install @nakshatra6350/api-diff
Enter fullscreen mode Exit fullscreen mode

The entire setup is 7 lines

import { init, defineSchema } from '@nakshatra6350/api-diff';

defineSchema('/api/users', {
  id:    { type: 'string' },
  name:  { type: 'string' },
  email: { type: 'string' },
});

init('warn');
Enter fullscreen mode Exit fullscreen mode

That's it. Drop this in your app's entry point. Every fetch call to /api/users is now contract-checked automatically.

When your backend ships a breaking change, you'll see this in your console immediately:

[api-diff] Contract drift on /api/users:
  • email: expected string, got missing
  • id: expected string, got number
Enter fullscreen mode Exit fullscreen mode

Before your users see anything. Before Sentry fires. Before the support tickets come in.


Three modes for three environments

init('warn');    // Development  — console.warn, non-blocking
init('throw');   // Tests / CI   — throws an Error, fails the suite
init('silent');  // Production   — silent collection, no noise
Enter fullscreen mode Exit fullscreen mode

In your test suite, flip it to throw mode and API drift becomes a failing test — it gets caught in CI before it ever reaches production.


Nested objects? Handled.

defineSchema('/api/loans', {
  loanId: { type: 'string' },
  amount: { type: 'number' },
  status: { type: 'string' },
  user: {
    type: 'object',
    fields: {
      id:   { type: 'string' },
      name: { type: 'string' },
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

The diff engine recurses into nested objects and reports the exact path that drifted — user.name: expected string, got missing — not just a vague "response mismatch."


How it works under the hood

  1. You call defineSchema() to register URL → schema pairs in an internal registry
  2. init() wraps globalThis.fetch with a thin interceptor
  3. Every fetch call passes through — if the URL matches a schema, the response is cloned (your app gets the original, untouched)
  4. The clone is parsed and deep-compared field by field, type by type
  5. Drift is reported based on your chosen mode
  6. Zero latency added to your actual requests The interceptor adds no overhead to unmatched requests and only clones responses on matched ones — the clone is a native browser API, not a re-fetch.

Why not just use Zod or OpenAPI validators?

Fair question. Here's how I think about it:

Tool What it's for
Zod Compile-time + runtime validation wired into your data layer. Great, but requires you to own and update the schema actively.
OpenAPI validators Full contract testing with generated specs. Powerful, but heavy — you need backend cooperation and a build step.
api-diff A lightweight safety net at the fetch layer. No backend changes, no code-gen, no build step. Drop it in, define what you expect, move on.

These tools aren't competitors. I use Zod for domain validation and api-diff as an early warning system at the network boundary.


Full TypeScript support, zero extra packages

import type { ApiSchema, DiffResult, DriftItem, DiffMode } from '@nakshatra6350/api-diff';
Enter fullscreen mode Exit fullscreen mode

Types ship with the package. No @types/ install needed.


What's coming in v0.2.0

  • Array item validation — define a schema for items inside array responses
  • onDrift callback — pipe drift events to Sentry, analytics, or your own endpoint

- Vite plugin — define schemas in a config file instead of in code

Links

  • 📦 npm → npmjs.com/package/@nakshatra6350/api-diff
  • 🐙 GitHub → github.com/nakshatra6350/api-diff Built this after 2.5 years of fintech frontend work watching silent API failures slip past TypeScript into production. If you've felt this pain, give it a try and open an issue with feedback — every response shape I haven't thought of yet is a bug I want to know about.

Top comments (0)