OpenAPI 3.0 & 3.1 · TypeScript · CLI

Know which API change breaks which client.

api-sentinel-js compares two OpenAPI specs, judges every change by whether it sits in a request or a response, and turns the findings into regression tests for the framework you already use.

npm install --save-dev api-sentinel-js

Framework-agnostic · v0.1.0 · MIT · Node.js 18.17+ · one runtime dependency

api-compatfailing2 breaking · 1 warning · 0 info
  • breakingAPS031POST /pets

    New required request property "species".

    Fix: Make the property optional with a default.

  • warningAPS037GET /pets/{id}

    Response enum can now return new values: "archived".

    Fix: Announce new values to clients, or expose them behind an opt-in.

  • breakingAPS038GET /pets/{id}

    null may now appear in responses where it could not before.

    Fix: Keep the previous nullability.

Why position matters

The same edit. A different verdict.

Adding a required property is harmless in a response and breaking in a request. Most diff tools flag both or neither. Each cell below was produced by running api-sentinel-js on that exact edit.

ChangeAs a requestAs a response
Add a required propertyBreakingAPS031Compatible
Add an enum valueCompatibleWarningAPS037
Widen integer to numberCompatibleBreakingAPS030
Allow nullCompatibleBreakingAPS038
Raise maxLengthCompatibleWarningAPS039

Read the full compatibility table

Playground

Edit either spec. The verdict updates as you type.

This runs the api-sentinel-js source from this repository in your browser. Nothing is mocked, and nothing is uploaded.

FAIL1 breaking · 0 warning · 0 info
  • breakingAPS031required-request-property-addedPOST /pets

    New required request property "species".

    at requestBody/application/json/schema/properties/speciesconfidence: high

    Fix: Make the property optional with a default.

Clients that do not send species now fail. Make it optional with a default. How verdicts are decided →

The code behind this panel
ts
import { compareApiSpecs, generateTests, renderTests } from "api-sentinel-js";

export type FailOn = "breaking" | "warning" | "info" | "none";

export function run(input: { base: string; candidate: string; failOn: FailOn }) {
  const result = compareApiSpecs({
    base: { content: input.base },
    candidate: { content: input.candidate },
    policy: { failOn: input.failOn },
  });
  const tests = renderTests(generateTests(result), "vitest");
  return { result, tests };
}

Findings become tests

Not just “what changed”. Also “what do I test?”

Every finding can produce HTTP scenarios: a legacy-client request, an omitted required field, a removed enum value, missing credentials, documented error shapes. They are rendered for vitest, jest or node:test with JSON Schema response assertions, and your framework runs them.

  • Positive: the old request must still succeed
  • Negative: a missing new field must be rejected
  • Auth: no credentials, insufficient scopes
  • Removed operations fail cleanly with 404, 405 or 410

How test generation works

api.regression.test.tsgenerated from the sample on this page
// Generated by APISentinel 0.1.0. Do not edit by hand; regenerate instead.
// Environment: APISENTINEL_BASE_URL, APISENTINEL_AUTH_<SCHEME> (credential values). Requires the "ajv" package.
import { describe, it } from "vitest";
import Ajv from "ajv";

const BASE_URL = process.env.APISENTINEL_BASE_URL ?? "http://localhost:3000";
const ajv = new Ajv({ strict: false, allErrors: true, validateFormats: false });

function envName(scheme) {
  return "APISENTINEL_AUTH_" + scheme.name.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
}

function applyAuth(auth, headers, url) {
  if (auth.mode === "none") return;
  for (const s of auth.schemes) {
    const raw = process.env[envName(s)];
    if (!raw) throw new Error("Set " + envName(s) + " to run this test (credential for scheme " + s.name + ").");
    if (s.type === "http" && s.scheme === "basic") headers["Authorization"] = "Basic " + raw;
    else if (s.type === "apiKey" && s.in === "query") url.searchParams.set(s.paramName, raw);
    else if (s.type === "apiKey" && s.in === "cookie") headers["Cookie"] = s.paramName + "=" + raw;
    else if (s.type === "apiKey") headers[s.paramName] = raw;
    else headers["Authorization"] = "Bearer " + raw;
  }
}

function statusMatches(status, allowed) {
…

What you get

Built to be trusted in CI.

  • Position-aware rules

    Request and response schemas are judged separately, so harmless changes stay quiet.

  • 38 documented rules

    Stable IDs, default severities, detection notes and remediation for every one.

  • Honest confidence

    Heuristic findings say so, with an explanation of what the spec cannot show.

  • Policy and baselines

    Override severities, ignore paths, and adopt the tool on an API with known breaks.

  • Safe on untrusted input

    Specs are never evaluated. Size, depth, alias and step limits keep it from hanging.

  • Typed and synchronous

    ESM and CommonJS, bundled types, one runtime dependency, and byte-stable JSON output.

In your pipeline

A CLI with exit codes you can build on.

sh
# Compare two specs. Exit code 1 when a breaking change is found.
npx api-sentinel-js compare openapi.old.yaml openapi.new.yaml

# Accept today's findings, then fail only on new ones.
npx api-sentinel-js compare old.yaml new.yaml --write-baseline .apisentinel-baseline.json
npx api-sentinel-js compare old.yaml new.yaml --baseline .apisentinel-baseline.json

# Findings to regression tests.
npx api-sentinel-js tests old.yaml new.yaml --framework vitest --output api.regression.test.ts
yaml
- run: git show "origin/${{ github.base_ref }}:openapi.yaml" > "$RUNNER_TEMP/openapi.base.yaml"
- run: npx --yes api-sentinel-js compare "$RUNNER_TEMP/openapi.base.yaml" openapi.yaml --fail-on breaking
ExitMeaning
0No new findings at or above the threshold
1Findings reached the threshold (default: breaking)
2Usage, policy or baseline error
3Invalid, unsupported or oversized spec
4Internal error, please report

CLI and policy reference

Documentation

Everything is written down.