Skip to content

Swagger/OpenAPI

Automatic interactive API documentation generation using OpenAPI specifications, powered by Scalar or Swagger UI.

Quick Reference

ItemValue
Package@venizia/ignis
ClassSwaggerComponent
UI FactoryUIProviderFactory
RuntimesBoth
ProviderValueWhen to Use
Scalar'scalar'Modern, clean UI (default)
Swagger UI'swagger'Classic Swagger interface

Import Paths

typescript
import { SwaggerComponent, SwaggerBindingKeys, UIProviderFactory } from '@venizia/ignis';
import type { ISwaggerOptions, IUIProvider, IUIConfig, IGetProviderParams } from '@venizia/ignis';

Setup

Step 1: Bind Configuration (Optional)

Skip this step to use the defaults (Scalar UI at /doc/explorer). To customize:

typescript
// In your Application class's preConfigure method (src/application.ts)
import { SwaggerBindingKeys, ISwaggerOptions } from '@venizia/ignis';

this.bind<ISwaggerOptions>({
  key: SwaggerBindingKeys.SWAGGER_OPTIONS,
}).toValue({
  restOptions: {
    base: { path: '/doc' },
    doc: { path: '/openapi.json' },
    ui: { path: '/explorer', type: 'swagger' }, // Use Swagger UI instead of Scalar
  },
  explorer: {
    openapi: '3.0.0',
  },
});

Step 2: Register Component

typescript
// src/application.ts
import { SwaggerComponent, BaseApplication, ValueOrPromise } from '@venizia/ignis';

export class Application extends BaseApplication {
  preConfigure(): ValueOrPromise<void> {
    // ...
    this.component(SwaggerComponent);
  }
}

Step 3: Define Routes with Zod Schemas

To get the most out of the documentation, define your routes with zod schemas:

typescript
// src/controllers/hello.controller.ts
import { z } from '@hono/zod-openapi';
import { BaseRestController, controller, jsonContent, ValueOrPromise } from '@venizia/ignis';
import { HTTP } from '@venizia/ignis-helpers';

@controller({ path: '/hello' })
export class HelloController extends BaseRestController {
  constructor() {
    super({ scope: HelloController.name, path: '/hello' });
  }

  override binding(): ValueOrPromise<void> {
    this.defineRoute({
      configs: {
        path: '/',
        method: 'get',
        responses: {
          [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
            description: 'A simple hello message',
            schema: z.object({ message: z.string() }),
          }),
        },
      },
      handler: (c) => {
        return c.json({ message: 'Hello, `Ignis`!' }, HTTP.ResultCodes.RS_2.Ok);
      },
    });
  }
}

TIP

Controllers using defineRoute with Zod schemas automatically generate OpenAPI specs. The Swagger component discovers all registered controller routes and renders them in the documentation UI.

Configuration

OptionTypeDefaultDescription
restOptions.base.pathstring'/doc'Base path for all documentation routes
restOptions.doc.pathstring'/openapi.json'Path to the raw OpenAPI spec (relative to base)
restOptions.ui.pathstring'/explorer'Path to the documentation UI (relative to base)
restOptions.ui.type'swagger' | 'scalar''scalar'UI provider type
explorer.openapistring'3.0.0'OpenAPI specification version
uiConfigRecord<string, any>undefinedCustom config passed to the UI provider

IMPORTANT

The explorer.info field is always overwritten during the component's binding() phase. The component unconditionally reads your application's package.json via application.getAppInfo() and sets explorer.info to { title, version, description, contact } from that data. Any user-provided explorer.info values are discarded. If you need to customize these fields, update your package.json instead.

NOTE

The explorer.servers field is auto-populated only when empty. If you provide explorer.servers with at least one entry, the component preserves your values. When no servers are configured, it creates a default entry from application.getServerAddress() plus the application base path.

ISwaggerOptions -- Full Reference

typescript
export interface ISwaggerOptions {
  restOptions: {
    base: { path: string };
    doc: { path: string };
    ui: { path: string; type: TDocumentUIType };
  };
  explorer: {
    openapi: string;
    info?: {
      title: string;
      version: string;
      description: string;
      contact?: { name: string; email: string };
    };
    servers?: Array<{
      url: string;
      description?: string;
    }>;
  };
  uiConfig?: Record<string, any>;
}
OptionTypeDefaultDescription
restOptions.base.pathstring'/doc'Base path for all documentation routes
restOptions.doc.pathstring'/openapi.json'Path to the raw OpenAPI spec (relative to base)
restOptions.ui.pathstring'/explorer'Path to the documentation UI (relative to base)
restOptions.ui.type'swagger' | 'scalar''scalar'UI provider type
explorer.openapistring'3.0.0'OpenAPI specification version
explorer.info.titlestringAlways from package.json nameAPI title (overwritten at runtime)
explorer.info.versionstringAlways from package.json versionAPI version (overwritten at runtime)
explorer.info.descriptionstringAlways from package.json descriptionAPI description (overwritten at runtime)
explorer.info.contact{ name, email }Always from package.json authorContact information (overwritten at runtime)
explorer.serversArray<{ url, description? }>Auto-detected when emptyServer URLs
uiConfigRecord<string, any>undefinedCustom config passed to the UI provider

IGetProviderParams Interface

The IGetProviderParams interface is used by UIProviderFactory.getProvider() and UIProviderFactory.register():

typescript
export interface IGetProviderParams {
  type: string;
}

This interface is exported for use when building custom tooling around the UIProviderFactory -- for example, programmatically querying which providers are available or registering providers in tests.

Tech Stack

LibraryPurpose
@hono/zod-openapiOpenAPI generation from Zod schemas
@hono/swagger-uiSwagger UI rendering
@scalar/hono-api-referenceScalar UI rendering
zodSchema validation and type generation

TIP

The component also auto-registers JWT (bearer) and Basic security schemes in the OpenAPI spec, so authenticated endpoints display the correct auth UI in the documentation.

Architecture

Component Lifecycle

The SwaggerComponent executes the following during binding():

  1. Resolve options -- reads SwaggerBindingKeys.SWAGGER_OPTIONS from DI using application.get() with isOptional: true, falls back to DEFAULT_SWAGGER_OPTIONS via the ?? operator if no binding exists
  2. Overwrite info -- unconditionally reads package.json via application.getAppInfo() and overwrites explorer.info with { title: appInfo.name, version: appInfo.version, description: appInfo.description, contact: appInfo.author }
  3. Auto-detect servers -- if explorer.servers is empty or unset, creates one entry from http:// + application.getServerAddress() + configs.path.base
  4. Normalize paths -- all path segments (base.path, doc.path, ui.path) are normalized to ensure a leading / is present, handling both /path and path inputs
  5. Register OpenAPI doc route -- calls rootRouter.doc(docPath, explorer) to register the raw JSON endpoint
  6. Resolve UI type with fallback -- evaluates restOptions.ui.type || DocumentUITypes.SWAGGER. Note: this means a falsy value (empty string) falls back to 'swagger', not 'scalar'
  7. Validate UI type -- checks the resolved type against DocumentUITypes.SCHEME_SET, throws if invalid
  8. Register UI provider -- calls UIProviderFactory.register({ type }) to instantiate the UI renderer
  9. Construct docUrl -- builds the full documentation URL by joining configs.path.base, configs.basePath, and the computed docPath
  10. Register UI route -- creates GET handler at uiPath that calls uiProvider.render() with { title: appInfo.name, url: docUrl, ...uiConfig }
  11. Register security schemes -- auto-registers JWT (bearer) and Basic security schemes in the OpenAPI registry

Architecture Components

ComponentClassRole
SwaggerComponentextends BaseComponentOrchestrates binding, overwrites OpenAPI metadata from package.json
UIProviderFactoryextends MemoryStorageHelper (singleton)Registry for UI providers, validates and instantiates
SwaggerUIProviderimplements IUIProviderRenders Swagger UI via @hono/swagger-ui
ScalarUIProviderimplements IUIProviderRenders Scalar UI via @scalar/hono-api-reference

UIProviderFactory and MemoryStorageHelper

UIProviderFactory extends MemoryStorageHelper<{ [key: string | symbol]: IUIProvider }>, which provides a simple in-memory key-value store with the following methods used internally:

  • isBound(key) -- checks if a provider type is already registered
  • get(key) -- retrieves a registered provider instance
  • set(key, value) -- stores a provider instance
  • keys() -- lists all registered provider type keys

This gives the factory a lightweight, type-safe storage backend without requiring the full DI container.

Lazy Dynamic Imports

Both SwaggerUIProvider and ScalarUIProvider use await import() inside their render() method to load the underlying UI library:

typescript
// SwaggerUIProvider
async render(context, config, next) {
  const { swaggerUI } = await import('@hono/swagger-ui');
  // ...
}

// ScalarUIProvider
async render(context, config, next) {
  const { Scalar } = await import('@scalar/hono-api-reference');
  // ...
}

This means UI libraries are loaded on the first HTTP request to the documentation endpoint, not at application startup. This keeps startup time fast and avoids loading unused UI libraries (only the configured provider's library is ever imported).

ScalarUIProvider Title Mapping

The ScalarUIProvider maps the title field to pageTitle when calling the Scalar renderer:

typescript
const { title, url, ...customConfig } = config;
return Scalar({ url, pageTitle: title, ...customConfig })(context, next);

This is a quirk to be aware of if you are inspecting the rendered output or writing custom UI providers -- Scalar uses pageTitle instead of title.

UIProviderFactory API

MethodSignatureDescription
getInstance()static () => UIProviderFactoryReturns singleton instance
register()(opts: { type: string }) => voidInstantiates and registers a UI provider (idempotent)
getProvider()(opts: IGetProviderParams) => IUIProviderReturns registered provider or throws
getRegisteredProviders()() => string[]Lists all registered provider type keys

register() Idempotency

The register() method is idempotent. If a provider of the given type is already registered, it logs a warning and returns without error:

typescript
register(opts: { type: string }): void {
  if (this.isBound(opts.type)) {
    this.logger
      .for(this.register.name)
      .warn('Skip registering BOUNDED Document UI | type: %s', opts.type);
    return;
  }
  // ... instantiate and store the provider
}

This means calling register({ type: 'scalar' }) multiple times is safe and will not create duplicate provider instances.

IUIProvider Interface

typescript
interface IUIProvider {
  render(context: Context, config: IUIConfig, next: Next): Promise<Response | void>;
}

interface IUIConfig {
  title: string;     // App name from package.json
  url: string;       // Full URL to OpenAPI JSON endpoint
  [key: string]: any; // Additional config from uiConfig option
}

Security Scheme Registration

The component auto-registers two OpenAPI security schemes:

typescript
// JWT Bearer
rootRouter.openAPIRegistry.registerComponent('securitySchemes', 'jwt', {
  type: 'http',
  scheme: 'bearer',
  bearerFormat: 'JWT',
});

// Basic Auth
rootRouter.openAPIRegistry.registerComponent('securitySchemes', 'basic', {
  type: 'http',
  scheme: 'basic',
});

This ensures routes using authStrategies: ['jwt'] or authStrategies: ['basic'] display the correct auth UI (lock icon + input fields) in the documentation.

Binding Keys

KeyConstantTypeRequiredDefault
@app/swagger/optionsSwaggerBindingKeys.SWAGGER_OPTIONSISwaggerOptionsNoSee below

The SwaggerComponent constructor creates a default binding using the Binding fluent API:

typescript
this.bindings = {
  [SwaggerBindingKeys.SWAGGER_OPTIONS]: Binding.bind<ISwaggerOptions>({
    key: SwaggerBindingKeys.SWAGGER_OPTIONS,
  }).toValue(DEFAULT_SWAGGER_OPTIONS),
};

Note that unlike HealthCheckComponent, SwaggerComponent does not pass initDefault: { enable: true, container: application } to BaseComponent. The default bindings stored in this.bindings are not automatically registered into the DI container. Instead, the binding() method reads from the container with isOptional: true and falls back to DEFAULT_SWAGGER_OPTIONS via the ?? operator.

Default value:

typescript
const DEFAULT_SWAGGER_OPTIONS: ISwaggerOptions = {
  restOptions: {
    base: { path: '/doc' },
    doc: { path: '/openapi.json' },
    ui: { path: '/explorer', type: 'scalar' },
  },
  explorer: {
    openapi: '3.0.0',
    info: {
      title: 'API Documentation',
      version: '1.0.0',
      description: 'API documentation for your service',
    },
  },
};

NOTE

The explorer.info values in DEFAULT_SWAGGER_OPTIONS are never used at runtime because binding() unconditionally overwrites explorer.info with data from package.json. They exist only as structural defaults.

Type Definitions

typescript
type TDocumentUIType = TConstValue<typeof DocumentUITypes>;

class DocumentUITypes {
  static readonly SWAGGER = 'swagger';
  static readonly SCALAR = 'scalar';
  static readonly SCHEME_SET: Set<string>;
  static isValid(input: string): boolean;
}

TDocumentUIType is derived via TConstValue, which extracts the union of all static readonly string values from DocumentUITypes. This ensures the type stays in sync with the constants automatically.

API Endpoints

MethodPathDescription
GET/doc/explorerDocumentation UI (Scalar by default)
GET/doc/openapi.jsonRaw OpenAPI specification

NOTE

These paths are based on the default configuration. If you customize restOptions.base.path, restOptions.ui.path, or restOptions.doc.path, the actual endpoints change accordingly.

Documentation UI Endpoint

Default path: /doc/explorer

Renders an interactive API documentation page using the configured UI provider (Scalar or Swagger UI). The UI fetches the OpenAPI spec from the JSON endpoint and renders it with full request/response exploration, authentication controls, and try-it-out functionality.

OpenAPI JSON Endpoint

Default path: /doc/openapi.json

Returns the raw OpenAPI JSON specification generated from all registered controller routes and their Zod schemas. This endpoint can be used by:

  • External API testing tools (Postman, Insomnia)
  • CI pipelines for API contract validation
  • Client SDK generators (openapi-generator, orval)
  • API gateway configuration

Troubleshooting

"Invalid document UI Type"

Cause: The restOptions.ui.type value is not 'swagger' or 'scalar'. The UIProviderFactory only recognizes these two built-in providers. Note that a falsy value (empty string, undefined) does not trigger this error -- it silently falls back to 'swagger' due to the || operator, not to the default 'scalar'.

Fix: Use a valid UI type:

typescript
this.bind<ISwaggerOptions>({
  key: SwaggerBindingKeys.SWAGGER_OPTIONS,
}).toValue({
  restOptions: {
    base: { path: '/doc' },
    doc: { path: '/openapi.json' },
    ui: { path: '/explorer', type: 'scalar' }, // 'scalar' or 'swagger'
  },
  explorer: { openapi: '3.0.0' },
});

Documentation UI shows no routes

Cause: Controllers are not defining routes with Zod schemas via defineRoute or bindRoute. Only routes registered through @hono/zod-openapi appear in the OpenAPI spec.

Fix: Use defineRoute with Zod response schemas in your controllers:

typescript
this.defineRoute({
  configs: {
    path: '/',
    method: 'get',
    responses: {
      200: jsonContent({
        description: 'Success',
        schema: z.object({ message: z.string() }),
      }),
    },
  },
  handler: (c) => c.json({ message: 'ok' }, 200),
});

"Unknown UI Provider"

Cause: The UIProviderFactory.getProvider() was called with a type that has not been registered. This typically happens if the component binding phase failed silently.

Fix: Ensure the SwaggerComponent is registered in preConfigure() and that no errors occur during its binding() phase. Check the application logs for warnings from UIProviderFactory.

OpenAPI spec missing authentication schemes

Cause: The SwaggerComponent auto-registers JWT and Basic security schemes. If the AuthenticationComponent is not registered, authenticated routes will not show auth UI in the documentation.

Fix: Register AuthenticationComponent before SwaggerComponent in preConfigure() to ensure auth strategies are available when the Swagger component configures security schemes.

explorer.info values not matching custom configuration

Cause: The SwaggerComponent unconditionally overwrites explorer.info with values from package.json during its binding() phase. Any values you set in explorer.info via the DI binding are discarded.

Fix: Update your project's package.json fields (name, version, description, author) to control what appears in the API documentation info section. The component reads these via application.getAppInfo().

See Also