@edirect/documentation
Self-contained NestJS module that serves code-first OpenAPI documentation at /docs, with a
bolttech app bar and pluggable renderers.
- Multi-renderer — Redoc (default), Scalar, or Swagger UI, picked per service.
- Version switching — declare multiple API versions; the viewer renders a version dropdown.
- Deprecated badge — mark a version
deprecated and it shows an amber badge in the app bar.
- bolttech app bar — one slim, sticky bar shared across renderers: logo, workspace breadcrumb,
environment badge, version switch, support link, and OpenAPI download.
- Code-first — specs are generated lazily from the live Nest application via
@nestjs/swagger;
no hand-written OpenAPI files to maintain.
Installation
pnpm add @edirect/documentation
Peer libraries used at runtime (already present in any Nest service): @nestjs/common,
@nestjs/swagger, express.
Quick start
The module is @Global. Register it in your AppModule, then bind the running app once after
bootstrap so declarative versions can be generated lazily.
import { Module } from '@nestjs/common';
import { DocumentationModule } from '@edirect/documentation';
@Module({
imports: [
DocumentationModule.forRoot({
title: 'Messaging Service',
workspace: 'Shared Services',
support: 'shared-services@bolttech.io',
versions: [
{ slug: 'v2', label: 'v2', version: '2.0', isDefault: true, paths: ['/v2'] },
],
}),
],
})
export class AppModule {}
import { NestFactory } from '@nestjs/core';
import { DocumentationService } from '@edirect/documentation';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.get(DocumentationService).useApp(app);
await app.listen(3000);
}
bootstrap();
Open http://localhost:3000/docs.
Async configuration
Use forRootAsync when options depend on config/DI. Because the resolved root value is unknown at
module-build time, opt into the / route with mountRoot: true.
DocumentationModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
mountRoot: true,
useFactory: (config: ConfigService) => ({
enabled: config.get('DOCS_ENABLED') !== 'false',
title: config.get('SERVICE_NAME'),
root: 'redirect',
versions: [{ slug: 'v2', isDefault: true, paths: ['/v2'] }],
}),
});
Routes
GET /docs | HTML viewer (renderer of your choice). Honors ?spec=<slug> to switch version. |
GET /docs/spec | JSON index of registered specs ({ default, specs: [{ slug, url, ... }] }). |
GET /docs/spec/:slug | OpenAPI JSON for a spec, with ETag + Cache-Control and 304 support. |
GET / | Only when root is redirect or serve (and mountRoot: true for async). |
When enabled: false, every docs route responds 404.
Options
enabled | boolean | true | Master switch. false → all docs routes return 404. |
title | string | 'API Documentation' | Page title and OpenAPI info.title. |
description | string | — | Default description for generated versions. |
workspace | string | — | Domain/workspace shown before the title in the breadcrumb. |
support | string | — | Support email or URL shown in the app bar. |
renderer | 'redoc' | 'scalar' | 'swagger-ui' | 'redoc' | Which viewer to render. |
logo | { url; altText? } | bolttech logo | Brand logo in the app bar. |
brand.accent | string (hex or CSS keyword) | bolttech cyan #00bac7 | Accent color; validated, invalid values are ignored. |
bearerAuth | boolean | true | Adds a bearer security scheme to every generated version. |
servers | string[] | — | Servers advertised in every generated version. |
root | 'off' | 'redirect' | 'serve' | 'off' | / behavior. Default never claims /. |
versions | DocumentationVersionOptions[] | — | Declarative versions, generated lazily from the app. |
specs | DocumentationSpec[] | — | Pre-built OpenAPI documents registered as-is. |
DocumentationVersionOptions: slug (required), label, description, version, order,
isDefault, enabled, deprecated, paths (keep only routes under these prefixes), include
(restrict generation to given Nest modules), configure(builder) (customize the DocumentBuilder).
Note: the actual runtime defaults are renderer: 'redoc' and root: 'off'.
Recipes
Multiple versions
DocumentationModule.forRoot({
title: 'Policy Service',
versions: [
{ slug: 'v2', label: 'v2', version: '2.0', order: 1, isDefault: true, paths: ['/v2'] },
{ slug: 'v1', label: 'v1', version: '1.0', order: 2, deprecated: true, paths: ['/v1'] },
],
});
Mark a version deprecated
Set deprecated: true on the version — the app bar shows an amber Deprecated badge when that
version is selected.
Custom accent / logo / workspace
DocumentationModule.forRoot({
workspace: 'Payments',
brand: { accent: '#7c3aed' },
logo: { url: 'https://example.com/logo.svg', altText: 'Acme' },
versions: [{ slug: 'v2', isDefault: true }],
});
Turn off in production
DocumentationModule.forRoot({
enabled: process.env.NODE_ENV !== 'production',
versions: [{ slug: 'v2', isDefault: true }],
});
Redirect root to the docs
DocumentationModule.forRoot({ root: 'redirect', versions: [{ slug: 'v2', isDefault: true }] });
Pre-built spec (escape hatch)
DocumentationModule.forRoot({
specs: [
{
slug: 'partner',
label: 'Partner API',
isDefault: true,
document: () => loadPartnerOpenApi(),
},
],
});
Security notes
- Pinned CDN + SRI — Redoc
2.5.3, Scalar 1.62.1, and swagger-ui-dist 5.32.8 are loaded
from jsDelivr at fixed versions with Subresource Integrity hashes (see renderers/shared.ts,
CDN). Versions cannot drift to latest, and a compromised CDN cannot inject script. When
bumping a renderer, change the version and the integrity hash together.
noindex — the viewer emits <meta name="robots" content="noindex, nofollow">.
enabled gate — set enabled: false to make every docs route return 404 (e.g. production).
- Auth — the module does not gate
/docs itself. If docs should require authentication, the
consumer scopes them in its own auth middleware/guard.
Gotcha
app.get(DocumentationService).useApp(app) is required in main.ts. Declarative versions
are generated lazily from the live app — without useApp, they cannot be built and the service logs
a warning shortly after boot. (specs registered as pre-built documents do not need it.)