@bamboo-studio/swaggerify
v0.0.6
Published
A swagger generation tool for bamboo atlas api scaffold
Downloads
272
Readme
Swaggerify
Automatic OpenAPI 3.0.3 documentation generator for TypeScript backends. Scans route definitions and controller decorators to produce a complete Swagger spec — no manual YAML needed.
How it works
Route file (AST scan)
→ routes with paths, methods, schema names
→ Controller instances (dynamic import + reflect-metadata)
→ Schema classes (dynamic import + class-validator)
→ OpenAPI 3.0.3 JSON- Route scanning —
scanRoutesFromSource()parses a TypeScript route class withts-morphand extracts paths, HTTP methods, and schema class names fromvalidationMiddleware()calls. - Controller enrichment —
enrichRoutesWithControllerMetadata()dynamically imports and instantiates controller classes, reads decorator metadata viareflect-metadata, and optionally resolves schema class names to JSON Schema objects. - Schema resolution —
scanSchemas()maps schema class names to file paths.classValidatorToObject()convertsclass-validator-decorated classes to JSON Schema. - OpenAPI generation —
routesToOpenAPI()assembles the full OpenAPI 3.0.3 document.
Installation
From npm
npm i @bamboo-studio/swaggerifyLocal (monorepo / file: reference)
# In the swaggerify project, build first:
npm run build
# In your consumer project:
npm install file:../path/to/swaggerifyYour tsconfig.json must enable decorator metadata:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}CLI Usage
The easiest way to use Swaggerify is the CLI. After installing and decorating your controllers and schemas, run:
npx swaggerify generateThis will scan your routes, enrich them with decorator metadata, and write swagger.json to the project root.
Verbose output
Pass -v / --verbose to print every detected route with its controller, tags, auth, body / query / params schemas (including resolved property names) and responses (status code, content type, body shape, description):
npx swaggerify generate -vExample output:
[swaggerify] Scanning routes in /app/src/routes
[swaggerify] src/routes/user.route.ts → 4 route(s)
[swaggerify] Found 4 route(s) in 1 file(s)
[swaggerify] Enriching with controller metadata...
[swaggerify] Routes
GET /users
controller UserController.getUsers
tags users
auth bearer
query UserListRequest → { firstName, email, page }
responses
200 application/json { results, total }
404 application/json { code, message } "Not found"
POST /users
controller UserController.createUser
body CreateUserRequest → { firstName*, email*, age }
responses
201 application/json { id, firstName, email }
400 application/json { code, message } "Bad request"
DELETE /users/:id
controller UserController.deleteUser
params { id }
responses
204 (no body) "No Content"
GET /users/:id/report.pdf
controller UserController.getReport
params { id }
responses
200 application/pdf string "PDF report"
[swaggerify] ✓ Generated /app/swagger.json (4 path(s))Colors and required-field markers (* next to required properties) are emitted when the output is a TTY.
Config file
Create swaggerify.config.ts in your project root to customise paths and metadata:
import type { SwaggerifyConfig } from 'swaggerify';
export default {
routesPath: 'src/routes', // default
controllersPath: 'src/controllers', // default
schemasPath: 'src/schemas', // default
output: 'swagger.json', // default
title: 'My API',
version: '1.0.0',
tags: [
{ name: 'Users', description: 'User management' },
{ name: 'Auth', description: 'Authentication' },
],
} satisfies SwaggerifyConfig;All fields are optional — defaults are shown above.
Requires
ts-nodein the consumer project to load TypeScript config and controller files:npm install --save-dev ts-node
Quick Start
import fs from 'fs';
import { scanRoutesFromSource, enrichRoutesWithControllerMetadata, routesToOpenAPI } from 'swaggerify';
// 1. Read your route file source
const routeCode = fs.readFileSync('./src/routes/user.route.ts', 'utf-8');
// 2. Extract routes via AST scanning
const routes = scanRoutesFromSource(routeCode);
// 3. Enrich with decorator metadata and resolve schema classes
const enrichedRoutes = enrichRoutesWithControllerMetadata(
'./src/controllers', // directory containing controller classes
routes,
'./src/schemas', // optional: directory containing schema classes
);
// 4. Generate OpenAPI spec
const openApi = routesToOpenAPI(enrichedRoutes, 'My API', '1.0.0', [{ name: 'Users', description: 'User management' }]);
fs.writeFileSync('openapi.json', JSON.stringify(openApi, null, 2));Route file conventions
Swaggerify expects route classes structured like this:
import { UserController } from '../controllers/user.controller';
import { UserListRequest, CreateUserRequest } from '../schemas/user.schema';
const validationMiddleware = (schema: any, pos: string) => {};
class UserRoute {
public path = '/users';
public router = new Router();
private userController = new UserController();
private initializeRoutes() {
// GET with query schema
this.router.get(`${this.path}`, validationMiddleware(UserListRequest, 'query'), this.userController.getUsers);
// POST with body schema
this.router.post(`${this.path}`, validationMiddleware(CreateUserRequest, 'body'), this.userController.createUser);
// GET with path params
this.router.get(`${this.path}/:id`, validationMiddleware(UserIdRequest, 'params'), this.userController.getUserById);
// @swaggerify-ignore
this.router.get(`${this.path}/internal-only`, this.userController.internalRoute);
}
}// @swaggerify-ignore
Place a // @swaggerify-ignore comment on the line immediately before a route definition to exclude it from the generated spec. No import required — it is a plain comment parsed by the AST scanner.
private initializeRoutes() {
// Included in the spec:
this.router.get(`${this.path}`, this.userController.getUsers);
// @swaggerify-ignore
this.router.post(`${this.path}/internal`, this.userController.internalAction);
// Also included:
this.router.delete(`${this.path}/:id`, this.userController.deleteUser);
}Decorators
Apply decorators to controller methods to enrich the generated documentation.
@Summary(text: string)
Sets the operation summary (short title).
@Summary('Get list of users')
public getUsers = async (req, res, next) => { ... };@Description(text: string)
Sets the operation description (longer explanation).
@Description('Returns a paginated list of users filtered by optional query parameters.')
public getUsers = async (req, res, next) => { ... };@SwaggerResponse(statusCode: number, SchemaClass?: Function, description?: string)
Declares a response. Can be applied multiple times to document multiple status codes.
@SwaggerResponse(200, UserListResponse)
public getUsers = async (req, res, next) => { ... };For body-less responses (e.g. 204 No Content) omit the schema. The generated operation will not include a content field.
@SwaggerResponse(204)
public deleteUser = async (req, res, next) => { ... };
// With description
@SwaggerResponse(204, 'No Content')
public deleteUser = async (req, res, next) => { ... };Non-JSON responses (PDF, CSV, binary, …)
Use the options form to declare a non-JSON content type:
// Binary (PDF, octet-stream, image, …) — default schema: { type: 'string', format: 'binary' }
@SwaggerResponse(200, { contentType: 'application/pdf', description: 'PDF report' })
public exportPdf = async (req, res, next) => { ... };
// Text content type (text/csv, text/plain, …) — default schema: { type: 'string' }
@SwaggerResponse(200, { contentType: 'text/csv' })
public exportCsv = async (req, res, next) => { ... };
// Explicit schema with custom contentType
@SwaggerResponse(200, { schema: ReportEnvelope, contentType: 'application/json' })
public getReport = async (req, res, next) => { ... };Multiple content types per status code
The array form accepts a contentType field. Stacking multiple entries on the same code merges them under one response:
@SwaggerResponse([
{ code: 200, schema: ReportJson, contentType: 'application/json' },
{ code: 200, contentType: 'application/pdf' },
])
public getReport = async (req, res, next) => { ... };Produces:
'200':
content:
application/json: { schema: { ... } }
application/pdf: { schema: { type: string, format: binary } }Stack multiple decorators to document different status codes:
@SwaggerResponse(200, UserListResponse)
@SwaggerResponse(404, ErrorResponse, 'User not found')
@SwaggerResponse(403, ErrorResponse, 'Forbidden')
public getUsers = async (req, res, next) => { ... };Alternatively, pass an array of response objects in a single decorator. schema is optional inside each entry:
@SwaggerResponse([
{ code: 200, schema: UserListResponse },
{ code: 204, description: 'No Content' },
{ code: 404, schema: ErrorResponse, description: 'User not found' },
])
public getUsers = async (req, res, next) => { ... };SchemaClass must be a class decorated with class-validator decorators.
@RequestBody(SchemaClass: Function)
Explicitly sets the request body schema. Takes priority over schemas found via route scanning.
@RequestBody(CreateUserRequest)
public createUser = async (req, res, next) => { ... };@Auth()
Marks the route as requiring Bearer token authentication. Adds security: [{ bearerAuth: [] }] to the operation.
@Auth()
@Summary('Get current user profile')
public getProfile = async (req, res, next) => { ... };@Tags(...tags: string[])
Overrides the auto-inferred tags for the route (default: derived from the route base path).
@Tags('users', 'admin')
public listAllUsers = async (req, res, next) => { ... };@Deprecated()
Marks the operation as deprecated in the OpenAPI spec (deprecated: true).
@Deprecated()
@Summary('Old user endpoint — use /v2/users instead')
public legacyGetUsers = async (req, res, next) => { ... };@Example(value: any) — schema decorator
Adds an example value to a schema class property. Works on any class used as request body, query params, or response schema. The example propagates correctly through class inheritance.
Apply to schema class properties (not controller methods):
import { Example } from 'swaggerify';
class CreateUserRequest {
@IsString()
@Example('John')
firstName: string;
@IsEmail()
@Example('[email protected]')
email: string;
@IsInt()
@IsOptional()
@Example(25)
age?: number;
}Produces:
{
"type": "object",
"properties": {
"firstName": { "type": "string", "example": "John" },
"email": { "type": "string", "format": "email", "example": "[email protected]" },
"age": { "type": "number", "example": 25 }
},
"required": ["firstName", "email"]
}@Example is also inherited — if a parent class has @Example on a property, child classes get it automatically without redeclaring.
@OpenApiOverride(json: Record<string, any>)
Provides a raw OpenAPI operation object that always takes priority over auto-generation. Use this when the generated spec is insufficient or incorrect.
If auto-generation fails and @OpenApiOverride is present, the override is used as a fallback.
@OpenApiOverride({
summary: 'Complex multi-format endpoint',
description: 'Returns data in multiple formats depending on Accept header.',
parameters: [
{ name: 'format', in: 'query', schema: { type: 'string', enum: ['json', 'csv'] } },
],
responses: {
200: { description: 'Success' },
400: { description: 'Invalid format' },
},
security: [{ bearerAuth: [] }],
})
public exportData = async (req, res, next) => { ... };Schema dynamic import
When Swaggerify scans a route file, schema names are stored as strings (e.g. 'UserListRequest'). To convert them to real JSON Schema objects, pass the schemaPath parameter to enrichRoutesWithControllerMetadata():
const enrichedRoutes = enrichRoutesWithControllerMetadata(
'./src/controllers',
routes,
'./src/schemas', // ← enables dynamic schema import
);Without schemaPath, schemas remain as unresolved strings — fine for manual mocking but will produce invalid OpenAPI output.
Schema classes use class-validator decorators:
import { IsString, IsEmail, IsOptional, IsInt, IsArray, IsEnum, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
export class UserListRequest {
@IsString()
@IsOptional()
firstName?: string;
@IsEmail()
@IsOptional()
email?: string;
@IsInt()
@IsOptional()
page?: number;
}
export class UserListResponse {
@IsArray()
@Type(() => UserDetail)
results: UserDetail[];
@IsInt()
total: number;
}Schema inheritance is supported — validators and @Example from parent classes are automatically included in the child schema:
class BasePagination {
@IsInt()
@IsOptional()
@Example(1)
page?: number;
@IsInt()
@IsOptional()
@Example(20)
pageLimit?: number;
}
class UserListRequest extends BasePagination {
@IsString()
@IsOptional()
firstName?: string;
}
// → produces properties: page, pageLimit, firstName (all optional)Supported validators:
| Decorator | JSON Schema output |
| --------------------------------------- | ------------------------------------- |
| @IsString() | { type: 'string' } |
| @IsInt() / @IsNumber() | { type: 'number' } |
| @IsBoolean() | { type: 'boolean' } |
| @IsEmail() | { type: 'string', format: 'email' } |
| @IsEnum(EnumType) | { type: 'string', enum: [...] } |
| @IsIn(any[]) | { type: 'any', enum: [...] } |
| @IsArray() + @Type(() => ItemClass) | { type: 'array', items: {...} } |
| @IsOptional() | Removes field from required[] |
@IsIn([]) needs another decorator to get the array item types, for example @IsInt()
Configuration
Use swagger.config.ts at project root for reusable settings:
import path from 'path';
export default {
controllersPath: path.resolve(__dirname, 'src/controllers'),
schemasPath: path.resolve(__dirname, 'src/schemas'),
tags: [
{ name: 'Users', description: 'User management' },
{ name: 'Auth', description: 'Authentication' },
],
info: {
title: 'My API',
version: '1.0.0',
},
};API Reference
scanRoutesFromSource(sourceCode: string): Route[]
Parses a TypeScript route class source string and returns an array of Route objects.
scanControllers(controllersPath: string): Record<string, string>
Returns a map of controller class names to their absolute file paths.
scanSchemas(schemasPath: string): Record<string, string>
Returns a map of schema class names to their absolute file paths.
enrichRoutesWithControllerMetadata(controllerPath, routes, schemaPath?): Route[]
Dynamically imports controllers, reads decorator metadata, and optionally resolves schema strings to JSON Schema objects.
classValidatorToObject(SchemaClass: Function): object
Converts a class-validator-decorated class to a JSON Schema object.
routesToOpenAPI(routes, title, version, tags): object
Generates an OpenAPI 3.0.3 document from enriched routes.
Route interface
interface Route {
path: string;
method: HttpMethods;
body?: object;
queryParams?: Record<string, object>;
params?: Record<string, object>;
auth?: boolean;
controller?: string;
function?: string;
responses?: { code: number; body: object }[];
summary?: string;
description?: string;
tags?: string[];
deprecated?: boolean;
openApiOverride?: Record<string, any>;
}TODOS
This library is still in what we can call beta stage. Here a list of todos:
- anonymous class support in @SwaggerResponse decorator
- better logging
