API Evolution
Routa keeps an accepted contract in .routa/openapi-baseline.json and compares every
generated document against it. That gives you a mechanical answer to “is this change
breaking?” and a deliberate step for accepting the change when it is intentional.
Before You Start
Section titled “Before You Start”- A committed
.routa/openapi-baseline.json, written byrouta scaffold. - A project that passes
routa check.
-
Check compatibility after changing a contract. Run this in CI on every pull request.
Terminal window npx routa openapi breakingTerminal window pnpm exec routa openapi breakingTerminal window yarn exec routa openapi breakingTerminal window bunx routa openapi breakingThe command fails for removed operations and for inputs that became required, whether the input is a query parameter, header, cookie, path parameter, or request body.
-
Deprecate rather than delete when an operation must go away. Declare the deprecation on the route contract.
src/routes/legacy/users/route.ts import { createRoute, createRouteRoot } from "@routa-ts/core";import { z } from "zod";const route = createRouteRoot("/legacy/users");export default route({get: createRoute({deprecation: {sunset: "2027-01-01",replacement: "/users",},responses: {success: {status: 200,schema: z.object({ message: z.string() }),},},run: () => ({type: "success",data: { message: "Use GET /users instead." },}),}),});Routa marks the operation deprecated in the generated OpenAPI. For a replacement inside the same API, use a leading
/and Routa verifies the target route exists. For another API, use an absolutehttp:orhttps:URL such ashttps://api.example.com/v2/users. -
Emit lifecycle headers if clients should learn about the deprecation at runtime.
src/routa.ts import { createRouta } from "@routa-ts/core";export default createRouta({lifecycleHeaders: true,});Deprecated operations then respond with
Deprecation: true, plusSunsetand aLinkwithrel="successor-version"when those are declared. -
Accept the change once it has been reviewed. This is the only command that rewrites the baseline.
Terminal window npx routa openapi breaking --update-baselineTerminal window pnpm exec routa openapi breaking --update-baselineTerminal window yarn exec routa openapi breaking --update-baselineTerminal window bunx routa openapi breaking --update-baseline
Verify
Section titled “Verify”A compatible change:
OpenAPI breaking check passed. No breaking changes detected.Accepting an intentional change:
OpenAPI baseline updated.A deprecated operation with lifecycleHeaders enabled:
HTTP/1.1 200 OKdeprecation: truesunset: 2027-01-01link: </users>; rel="successor-version"Which Check to Use
Section titled “Which Check to Use”| Command | Fails on | Use it |
|---|---|---|
routa openapi check |
Any difference from the baseline | Locally, to see that a change was noticed |
routa openapi breaking |
Removed operations and newly required inputs | In CI, as a merge gate |
openapi check is strict by design: adding an optional field trips it. That makes it a
good local signal and a poor merge gate, which is what openapi breaking is for.
Security Changes Are Contract Changes
Section titled “Security Changes Are Contract Changes”Middleware can describe the security an operation requires without Routa implementing any of it:
import { createMiddleware } from "@routa-ts/core";
createMiddleware({ openapi: { security: [{ bearerAuth: [] }], permissions: ["users.read"], }, // Application-owned credential and permission logic.});Routa emits these as OpenAPI security and x-routa-authz. Because they are part of the
document, an operation that was public becoming authenticated is reported by
routa openapi breaking like any other breaking change.
Troubleshooting
Section titled “Troubleshooting”| Problem | Fix |
|---|---|
Missing .routa/openapi-baseline.json. Run routa scaffold first. |
The project has no accepted contract. Run routa scaffold against the reviewed OpenAPI document first. |
ROUTA_DEPRECATION_REPLACEMENT_ROUTE_NOT_FOUND |
The local replacement path is not a route in this project. Check the path, or run routa generate if metadata is stale. |
ROUTA_DEPRECATION_REPLACEMENT_URL_INVALID |
Use /local-path for this API or an absolute http(s) URL for another. |
| Lifecycle headers do not appear | lifecycleHeaders defaults to off. Enable it in createRouta(), and confirm the route declares deprecation. |
| The baseline changes on every run | Generation is deterministic. A repeating diff means the source really is changing, often from stale .routa/routes.gen.ts. |