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-jsFramework-agnostic · v0.1.0 · MIT · Node.js 18.17+ · one runtime dependency
- breaking
APS031POST /petsNew required request property "species".
Fix: Make the property optional with a default.
- warning
APS037GET /pets/{id}Response enum can now return new values: "archived".
Fix: Announce new values to clients, or expose them behind an opt-in.
- breaking
APS038GET /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.
| Change | As a request | As a response |
|---|---|---|
| Add a required property | BreakingAPS031 | Compatible |
| Add an enum value | Compatible | WarningAPS037 |
| Widen integer to number | Compatible | BreakingAPS030 |
| Allow null | Compatible | BreakingAPS038 |
| Raise maxLength | Compatible | WarningAPS039 |
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.
- breaking
APS031required-request-property-addedPOST /petsFix: 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
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
// 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.
# 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- 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| Exit | Meaning |
|---|---|
0 | No new findings at or above the threshold |
1 | Findings reached the threshold (default: breaking) |
2 | Usage, policy or baseline error |
3 | Invalid, unsupported or oversized spec |
4 | Internal error, please report |
Documentation