Search docs

Jump between documentation pages.

Browse docs

API lifecycle & breaking changes

Because every DaloyJS endpoint is a single source of truth, the framework can answer two questions that usually need extra tooling: "how do I tell consumers an endpoint is going away?" and "did this change break my published API?" The first is solved with a route-level deprecation lifecycle; the second with an OpenAPI diff you can run in CI.

Deprecating a route

Set deprecated: true on a route to mark it in the OpenAPI document (the operation gets deprecated: true) and to emit an RFC 8594 Deprecation: true response header on every response from that route.

ts
app.route({
  method: "GET",
  path: "/v1/reports",
  deprecated: true,
  responses: { 200: { description: "OK" } },
  handler: () => ({ status: 200, body: { ok: true } }),
});

// Response headers:
//   Deprecation: true

Scheduling a sunset date

Add a sunset date to announce when the route will be removed. It accepts an ISO-8601 string, any string new Date(...) can parse, or a Date. A route with a sunsetis implicitly deprecated, so you don't need to set both.

ts
app.route({
  method: "GET",
  path: "/v1/reports",
  sunset: "2026-12-31T00:00:00Z",
  responses: { 200: { description: "OK" } },
  handler: () => ({ status: 200, body: { ok: true } }),
});

// Response headers (RFC 8594):
//   Deprecation: true
//   Sunset: Thu, 31 Dec 2026 00:00:00 GMT

The Sunset value is normalized to an IMF-fixdate (HTTP date) once, at app.route(...) registration time, so a typo fails fast instead of silently shipping a malformed header. The OpenAPI operation also carries the normalized value as an x-sunset vendor extension. If your handler sets its own Deprecation or Sunset header, the framework never overwrites it.

Detecting breaking changes

diffOpenAPI(baseline, current) compares two OpenAPI 3.x documents and classifies every difference as breaking (a consumer relying on the baseline could now fail) or non-breaking (additive or informational). It is pure and dependency-free, so it runs anywhere you can read two JSON files.

ts
import { diffOpenAPI, hasBreakingChanges } from "@daloyjs/core";
// or the focused entry point:
// import { diffOpenAPI } from "@daloyjs/core/openapi-diff";

const result = diffOpenAPI(publishedSpec, currentSpec);
// result.breaking:    OpenAPIChange[]
// result.nonBreaking: OpenAPIChange[]

if (hasBreakingChanges(publishedSpec, currentSpec)) {
  throw new Error("This change breaks the published API contract.");
}

The diff flags these as breaking:

  • a path or operation (HTTP method) present in the baseline is removed;
  • a documented response status code is removed from an operation;
  • a new required parameter is added to an existing operation;
  • an existing optional parameter becomes required;
  • an operation's request body becomes required when it was not.

New paths, operations, response codes, and optional parameters, parameter removals, a newly deprecated operation, and an info.version bump are all reported as non-breaking.

The daloy diff CLI

The same engine ships as a CLI command so you can gate any two spec files without writing code. It prints the classified changes and exits 1 when a breaking change is found.

bash
# Compare the last published spec against the freshly generated one
daloy diff openapi.published.json openapi.json

# Machine-readable output for CI
daloy diff --json openapi.published.json openapi.json

Wiring it into CI

Commit your published spec as a baseline (e.g. generated/openapi.baseline.json) and run the verify:breaking-changes gate. It compares the baseline against the freshly generated generated/openapi.json and fails the build on any breaking change. When no baseline exists yet the gate is a no-op, so you can adopt it incrementally.

bash
pnpm gen                      # regenerate generated/openapi.json
pnpm verify:breaking-changes  # fail CI if the published contract is broken

See also OpenAPI generation for how the spec is produced and typed clients for how consumers pick up the contract.