fastify-xswagger
fastify-xswagger
Route-scoped Swagger/OpenAPI documentation for Fastify v5. Define one or more doc scopes — each scoped to a route prefix with its own Swagger UI, access level (public or Basic-Auth-protected), and title. Docs are isolated per scope so /public and /admin routes never bleed into each other.
Installation
npm install @xenterprises/fastify-xswagger fastify@5
fastify@^5.0.0 is a peer dependency.
Quick Start
import Fastify from "fastify";
import xSwagger from "@xenterprises/fastify-xswagger";
const fastify = Fastify({ logger: true });
fastify.get("/public/users", {
schema: { tags: ["users"], description: "List users" },
}, async () => ({ users: [] }));
await fastify.register(xSwagger, {
docs: [{ prefix: "/public", access: "public", title: "Public API" }],
});
await fastify.listen({ port: 3000 });
// Docs UI: http://localhost:3000/public/documentation
// Spec JSON: http://localhost:3000/public/documentation/json
With private (Basic Auth) docs:
await fastify.register(xSwagger, {
docs: [
{ prefix: "/public", access: "public", title: "Public API" },
{ prefix: "/admin", access: "private", title: "Admin API" },
],
// Credentials come from the consumer — the plugin never reads process.env
auth: {
username: process.env.DOCS_USER,
password: process.env.DOCS_PASSWORD,
},
environment: process.env.NODE_ENV,
disableInProduction: "public",
});
Options
Plugin Options
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
docs | DocConfig[] | — | Yes | Documentation configurations (non-empty array, unique prefixes) |
auth | { username, password } | — | When any doc is private | Basic Auth credentials for private doc UIs |
disableInProduction | boolean | 'public' | false | No | Which docs to skip when environment === 'production' |
environment | string | 'development' | No | Environment name; only 'production' activates disableInProduction |
docsPath | string | '/documentation' | No | Path appended to each prefix for the Swagger UI |
active | boolean | true | No | Set false to skip registration entirely |
DocConfig
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
prefix | string | — | Yes | Route prefix to document (e.g. '/api', '/admin'); must be unique within docs |
title | string | — | Yes | Title shown in the Swagger UI header |
access | 'public' | 'private' | — | Yes | 'private' requires Basic Auth to view the docs |
version | string | '1.0.0' | No | API version shown in the spec |
description | string | API documentation for <prefix> routes | No | Description shown in the spec info |
Environment Controls
The plugin never reads process.env. The consumer sets env vars and passes the values in via app.register(xSwagger, { ... }) — typically environment: process.env.NODE_ENV. disableInProduction applies only when environment is exactly 'production':
| Value | Non-production | environment: 'production' |
|---|---|---|
false (default) | All docs enabled | All docs enabled |
true | All docs enabled | All docs disabled |
'public' | All docs enabled | Only private docs enabled |
Routes
For each enabled doc config, the plugin serves (via @fastify/swagger-ui inside an encapsulated scope):
| Method | Path | Purpose |
|---|---|---|
| GET | <prefix><docsPath> | Swagger UI (Basic Auth when access: 'private') |
| GET | <prefix><docsPath>/json | OpenAPI spec JSON (Basic Auth when access: 'private') |
| GET | <prefix><docsPath>/static/* | Swagger UI assets (Basic Auth when access: 'private') |
Route filtering: each doc's spec only includes routes under its prefix. Routes with schema.hide: true are always excluded.
Accessors
fastify.xSwagger.config
- config — Read-only config summary (environment, doc count, auth status). Auth credentials are never exposed.
fastify.xSwagger.docs
- docs[key].spec() — Per-doc instance properties and live OpenAPI spec accessor.
Error Reference
Registration fails fast with descriptive errors. Messages name the plugin, the option, and a usage example:
| Error (prefix) | Trigger |
|---|---|
xswagger: missing required option \docs` (non-empty array), e.g. ...` | docs missing, empty, or not an array |
xswagger: option \docsN` must be an object, e.g. ...` | A doc entry is not an object |
xswagger: option \docsN.prefix` must be a non-empty string, e.g. ...` | Missing/invalid prefix |
xswagger: option \docsN.title` must be a non-empty string, e.g. ...` | Missing/invalid title |
xswagger: option \docsN.access` must be 'public' or 'private', e.g. ...` | Invalid access level |
xswagger: option \docsN.version` must be a string, e.g. ...` | Non-string version |
xswagger: option \docsN.description` must be a string, e.g. ...` | Non-string description |
xswagger: option \docs` must not contain duplicate prefixes, e.g. ...` | Two docs share a prefix |
xswagger: option \auth` ... is required when using private docs, e.g. ...` | Private docs without valid auth.username/auth.password |
xswagger: option \docsPath` must be a non-empty string, e.g. ...` | Invalid docsPath |
xswagger: option \disableInProduction` must be true, false, or 'public', e.g. ...` | Invalid disableInProduction |
xswagger: option \environment` must be a string, e.g. ...` | Non-string environment |
xswagger: option \active` must be a boolean, e.g. ...` | Non-boolean active |
Private doc routes return 401 with a WWW-Authenticate: Basic realm="API Documentation" header when credentials are missing or wrong. Credentials are compared with crypto.timingSafeEqual.
Environment Variables
The plugin never reads process.env directly. These are consumer-side conventions — set them in your app and pass the values into app.register(xSwagger, { ... }):
| Variable | Required | Description |
|---|---|---|
NODE_ENV | No | Pass as environment — the value 'production' activates disableInProduction |
DOCS_USER | When private docs | Username for Basic Auth — pass as auth.username |
DOCS_PASSWORD | When private docs | Password for Basic Auth — pass as auth.password |
Exported Constants
import xSwagger, {
ACCESS_LEVELS, // { PUBLIC: 'public', PRIVATE: 'private' }
DEFAULT_DOCS_PATH, // '/documentation'
DISABLE_MODES, // { ALL: true, PUBLIC_ONLY: 'public', NONE: false }
DEFAULT_VERSION, // '1.0.0'
} from "@xenterprises/fastify-xswagger";
How It Works
On registration all options are validated at startup. For each enabled doc config (after disableInProduction + environment evaluation), @fastify/swagger and @fastify/swagger-ui are registered inside an encapsulated Fastify scope so their decorators don't conflict across instances. A transform function filters the generated OpenAPI spec to include only routes whose paths start with the doc's prefix. Private docs receive an onRequest UI hook that validates Basic credentials using crypto.timingSafeEqual to prevent timing attacks. The fastify.xSwagger decorator exposes a config summary and per-doc instances — auth credentials are never stored in the exposed config.
Requirements
- Node.js >= 20
- Fastify ^5.0.0 (peer dependency)
@fastify/swagger-ui^6.1.0 minimum, with@fastify/static^10.1.2 (enforced via the package'soverrides) — earlier versions are affected by a route-guard bypass advisory that let unauthenticated requests reach protected doc assets.
AI Context
package: "@xenterprises/fastify-xswagger"
type: fastify-plugin
use-when: Route-scoped Swagger/OpenAPI docs — multiple doc scopes per prefix, public or Basic-Auth-protected, environment-aware
decorator: fastify.xSwagger (config, docs[key].spec())
env: NODE_ENV (pass as environment), DOCS_USER/DOCS_PASSWORD (pass as auth.username/auth.password) — plugin never reads process.env
disableInProduction: false (all on) | true (all off) | "public" (only private docs when environment === 'production')
docs-path: {prefix}/documentation (configurable via docsPath)
requirements: Node >=20, fastify ^5, @fastify/swagger-ui ^6.1.0 + @fastify/static ^10.1.2 override (route-guard bypass fix)
