API versioning
DaloyJS supports URL-based API versioning with ordinary route prefixes. Put a stable major version in the public path, such as /api/v1/books, and mount that version with app.group() or a prefixed plugin. The resulting paths are normal DaloyJS routes: request and response validation, OpenAPI, generated clients, hooks, auth, and rate limits all continue to work.
Use a new major path only for a breaking contract. Additive endpoints and optional inputs normally stay in the current version. This keeps consumers stable without turning every release into another permanent API surface.
Create /api/v1/books
A group prepends its path to every route registered inside the callback. Group tags also carry into OpenAPI, which keeps versions easy to distinguish in Scalar, Swagger UI, or Redoc.
Groups are ideal for a small API or a version defined in one module. For a larger public API, prefer one plugin per major version so route ownership and version-specific middleware stay explicit.
Organize major versions as plugins
Keep version-specific transport contracts near each other, but call shared domain services underneath them. Avoid copying database and business logic into every version. Usually only the route schemas and the mapping between those schemas and the domain model should differ.
Each plugin registers resource paths such as /books. The prefix supplied by the application produces /api/v1/books and /api/v2/books. Plugins also encapsulate hooks and decorations, so a compatibility adapter or limit added to v1 does not leak into v2.
When to create a new major version
Treat the public request and response contract as the versioned product. Internal refactors, database migrations, and deployment changes do not need a new URL when callers observe the same behavior.
| Change | Typical decision |
|---|---|
| Add a new endpoint or optional request parameter | Keep the current major version |
| Fix internals without changing the contract | Keep the current major version |
| Remove or rename a request or response field | Create a new major version |
| Tighten accepted input so valid old requests fail | Create a new major version |
| Change the meaning of a field or operation | Create a new major version |
| Add a new required permission to an existing operation | Usually create a new major version or run a migration period |
Run DaloyJS's OpenAPI diff in CI instead of relying only on judgment. It catches removed paths and responses, newly required parameters, optional parameters that became required, and newly required request bodies.
OpenAPI and generated clients
A single DaloyJS application produces one OpenAPI document containing every public route registered on that application. If v1 and v2 run together, the default document contains both sets of paths. Use version-specific tags and globally unique operation IDs:
The OpenAPI info.version identifies the release of the document or API contract. It does not add a URL prefix and it does not select a route at runtime. Likewise, the route-level version property is informational metadata; it is not a routing switch and is not emitted into OpenAPI. The URL prefix is what makes /api/v1 real.
Current boundary: generateOpenAPI() does not currently filter a document by tag or version. If consumers need separately published v1 and v2 specifications or SDKs, register the reusable version plugin on a dedicated contract app for each codegen run.Reusing the same plugin for the runtime app and contract app prevents a second handwritten route inventory from drifting away from production. See OpenAPI generation and typed-client codegen for the publication workflow.
Retire an old version safely
Do not replace v1 in place when v2 is breaking. Run both versions, announce the migration, observe remaining v1 traffic, and remove v1 only after the published support window.
- Ship v2 while v1 remains available.
- Publish migration examples and a concrete retirement date.
- Mark v1 routes with
sunset, which also marks them deprecated. - Track usage by version and contact active consumers.
- Remove v1 after the date and the promised support window.
Read API lifecycle & breaking changes for the response headers, OpenAPI extensions, CLI diff, and CI gate.
Authentication, quotas, and paid APIs
Versioning keeps a customer's integration stable. It does not by itself turn an endpoint into a commercial API product. DaloyJS provides the data-plane building blocks that run on every request:
- bearer, JWT, JWK, mTLS, and HTTP-signature verification;
requireScopes()for operation-level permissions;rateLimit()with a Redis-backed store for multi-instance request limits;- structured logs, metrics, tracing, and request IDs;
- OpenAPI documentation, generated clients, and contract-change gates.
Your application or an external API-management control plane still owns customer onboarding, credential issuance and revocation, plan entitlements, billing, invoices, a developer portal, and durable usage records. Those policies are business-specific and are not a turnkey DaloyJS subsystem.
Security: a group'sauthoption documents the OpenAPI security requirement; it does not verify a credential by itself. Install an enforcement hook such asjwk(),bearerAuth(), or a reviewed custom API-key hook. Key limits by a stable authenticated customer ID, not by the raw secret, and use a shared store when more than one instance serves traffic.
For the enforcement pieces, continue with OAuth2/OIDC architecture, JWT and auth safeguards, and the Redis rate-limit store.
Supported versioning styles
| Style | DaloyJS support | Guidance |
|---|---|---|
URL path: /api/v1/books | First-class through groups and plugin prefixes | Recommended for public APIs |
Header: Accept-Version: 1 | No automatic route negotiation | Implement explicit application logic only when required |
| Versioned media types | No automatic route negotiation | Useful only when your API contract already requires it |
Query string: ?version=1 | Possible as ordinary input, not a versioning feature | Avoid for public contracts because caches and docs are less clear |
URL path versioning is deliberately boring: it is visible in logs, caches, documentation, generated clients, gateway policies, and support tickets. For a public resource API, that clarity is usually more useful than implicit negotiation.