Swagger & OpenAPI 3.1 Documentation
ExisJS comes with a native, built-in Swagger & OpenAPI 3.1 documentation engine. It automatically discovers all registered routes, introspects Tex and Zod validation schemas, and serves interactive API documentation at /docs.
Quick Start
Enable Swagger documentation in your exis.config.ts:
import { defineConfig } from 'exisjs/config'
export default defineConfig({ docs: { enabled: true, // Mounts UI at /docs and spec at /docs/json title: 'Store API Documentation', version: '1.0.0', description: 'Auto-generated API documentation with native schema validation', ui: 'swagger-ui', // 'swagger-ui' | 'scalar' }})Once enabled:
- Visit
http://localhost:3000/docsto view the interactive documentation UI. - Visit
http://localhost:3000/docs/json(or/docs/openapi.json) for the raw OpenAPI 3.1 specification.
Interactive Documentation UIs
ExisJS supports two built-in UI renderers:
1. Swagger UI (ui: 'swagger-ui', default)
The industry-standard interactive Swagger explorer featuring Try it out, live request execution, and parameter inspection.
2. Scalar API Reference (ui: 'scalar')
A modern, high-performance, dark-mode-first API reference documentation viewer with integrated code snippets and fast search.
import { defineConfig } from 'exisjs/config'
export default defineConfig({ docs: { enabled: true, ui: 'scalar', // Switches UI to modern Scalar }})Documenting Functional Routes
In ExisJS folder-based routing, each endpoint folder contains a route.ts file that exports a default controller({ ... }). You can add OpenAPI metadata directly to each route definition:
import { controller, route } from 'exisjs/router'import { tex, getPaginationSkip } from 'exisjs/validator'
export default controller({ list: route.get('/', { summary: 'List active users', description: 'Returns a paginated list of all active users in the system.', tags: ['Users'], query: tex.pagination({ defaultLimit: 25, maxLimit: 100 }), responses: { 200: { description: 'Paginated user list', schema: tex.object({ data: tex.array(tex.object({ id: tex.uuid(), name: tex.string() })), total: tex.number(), }), }, 400: { description: 'Invalid query parameters' }, }, async handle({ query }) { const { skip, limit } = getPaginationSkip(query) return { data: [ { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Alice' }, { id: '550e8400-e29b-41d4-a716-446655440001', name: 'Bob' }, ], total: 2, skip, limit, } }, }),
create: route.post('/', { summary: 'Create a new user', tags: ['Users'], body: tex.object({ name: tex.string({ min: 2 }), email: tex.email(), }), responses: { 201: { description: 'User successfully created' }, 400: { description: 'Validation error' }, }, async handle({ body }) { return { id: '550e8400-e29b-41d4-a716-446655440002', ...body } }, }),
getById: route.get('/:id', { summary: 'Get user by UUID', tags: ['Users'], params: tex.object({ id: tex.uuid() }), responses: { 200: { description: 'User details found' }, 404: { description: 'User not found' }, }, async handle({ params }) { return { id: params.id, name: 'Alice' } }, }),})Documenting Class-Based Controllers (OOP)
For class-based controllers in route.ts, use OpenAPI decorators from exisjs/swagger alongside standard routing decorators:
import { Controller, Get, Post, Body, Param, Query } from 'exisjs/decorators'import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiExclude,} from 'exisjs/swagger'
@ApiTags('Products')@ApiBearerAuth()@Controller()export default class ProductsController { @Get('/') @ApiOperation({ summary: 'List products', description: 'Fetches paginated list of catalog products', }) @ApiResponse({ status: 200, description: 'List of products' }) async list(@Query('page') page: number = 1) { return [ { id: 1, name: 'Mechanical Keyboard', price: 99 }, { id: 2, name: 'Ergonomic Mouse', price: 49 }, ] }
@Post('/') @ApiOperation({ summary: 'Create new product' }) @ApiResponse({ status: 201, description: 'Product created' }) async create(@Body() dto: { name: string; price: number }) { return { id: 3, ...dto } }
@Get('/:id') @ApiOperation({ summary: 'Get product by ID' }) @ApiResponse({ status: 200, description: 'Product found' }) @ApiResponse({ status: 404, description: 'Product not found' }) async getById(@Param('id') id: string) { return { id, name: 'Mechanical Keyboard', price: 99 } }
@Get('/internal/metrics') @ApiExclude() // Excluded from Swagger documentation async metrics() { return { uptime: process.uptime() } }}Cascading Boundary Metadata
Cascading boundaries (boundary.ts) can assign shared tags, descriptions, or security requirements to all nested routes in the directory:
import { defineBoundary } from 'exisjs/router'
export const config = defineBoundary({ tags: ['Admin Portal'], security: [{ bearerAuth: [] }],})Security Schemes & Authentication
Define reusable security schemes (such as Bearer JWT, API Keys, or OAuth2) in exis.config.ts:
import { defineConfig } from 'exisjs/config'
export default defineConfig({ docs: { enabled: true, securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', description: 'Enter your JWT token: Bearer <token>', }, apiKeyAuth: { type: 'apiKey', in: 'header', name: 'X-API-KEY', }, }, security: [ { bearerAuth: [] }, ], },})Advanced Configuration (exis.config.ts)
For advanced customization, such as modifying the generated OpenAPI spec before serving or injecting custom styling:
import { defineConfig } from 'exisjs/config'
export default defineConfig({ docs: { enabled: true, title: 'Enterprise API Gateway', version: '2.0.0', description: 'Internal and external microservices API reference', ui: 'scalar', // 'swagger-ui' | 'scalar' servers: [ { url: 'https://api.example.com', description: 'Production Server' }, { url: 'http://localhost:3000', description: 'Local Development' }, ], transformSpec(spec) { // Programmatically mutate or enrich OpenAPI specification spec.info.contact = { name: 'API Support', email: '[email protected]' } return spec }, },})Server Lifecycle Hook (server.ts)
You can also interact with the Swagger subsystem or application instance during server boot in src/http/server.ts:
Functional Paradigm
import { exis } from 'exisjs'
export default exis({ onStart(app) { console.log('[API] Swagger documentation ready at /docs') },})Class-Based (OOP) Paradigm
import { Server } from 'exisjs/decorators'import type { App } from 'exisjs'
@Server()export default class RootServer { async onStart(app: App) { console.log('[API] Swagger documentation ready at /docs') }}