X Enterprises

fastify-xswagger

Route-scoped Swagger/OpenAPI documentation for Fastify with Basic Auth access control and environment-aware availability.

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

NameTypeDefaultRequiredDescription
docsDocConfig[]YesDocumentation configurations (non-empty array, unique prefixes)
auth{ username, password }When any doc is privateBasic Auth credentials for private doc UIs
disableInProductionboolean | 'public'falseNoWhich docs to skip when environment === 'production'
environmentstring'development'NoEnvironment name; only 'production' activates disableInProduction
docsPathstring'/documentation'NoPath appended to each prefix for the Swagger UI
activebooleantrueNoSet false to skip registration entirely

DocConfig

NameTypeDefaultRequiredDescription
prefixstringYesRoute prefix to document (e.g. '/api', '/admin'); must be unique within docs
titlestringYesTitle shown in the Swagger UI header
access'public' | 'private'Yes'private' requires Basic Auth to view the docs
versionstring'1.0.0'NoAPI version shown in the spec
descriptionstringAPI documentation for <prefix> routesNoDescription 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':

ValueNon-productionenvironment: 'production'
false (default)All docs enabledAll docs enabled
trueAll docs enabledAll docs disabled
'public'All docs enabledOnly private docs enabled

Routes

For each enabled doc config, the plugin serves (via @fastify/swagger-ui inside an encapsulated scope):

MethodPathPurpose
GET<prefix><docsPath>Swagger UI (Basic Auth when access: 'private')
GET<prefix><docsPath>/jsonOpenAPI 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, { ... }):

VariableRequiredDescription
NODE_ENVNoPass as environment — the value 'production' activates disableInProduction
DOCS_USERWhen private docsUsername for Basic Auth — pass as auth.username
DOCS_PASSWORDWhen private docsPassword 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's overrides) — 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)
Copyright © 2026