swagger-og
v0.0.2
Published
Convention-based OpenAPI generation from Koa routes and TypeScript contract types
Maintainers
Readme
swagger-og
Convention-based OpenAPI generation for Koa apps. An endpoint appears in Swagger only when the controller handler wires a response type. Contract files hold type definitions; they are not enough on their own.
Integration guide
How types are resolved (read this first)
swagger-og scans routes, then the matching controller handler.
Visible in Swagger only if that handler is correctly configured, typically:
const response: GetMyAttendanceResponse = { success: true, data }; ctx.body = response;After that reference is found, swagger-og loads the type definition from files matching
contractFiles, then follows imports intotypeRoots(domain types).Not visible:
ctx.body = { success: true, data }with no response type. The route still works; it is just omitted from Swagger.Incorrectly configured: a
*Responsetype is exported in contract files but the controller never wires it. Wire it, or delete the unused export.
There is no public flag. Wiring the type in the controller is what makes an endpoint visible.
Type definitions are not hardcoded to src/contracts/api/. Each service sets contractFiles in its own openapi.config.ts.
Install
npm install swagger-og
npm install koa2-swagger-ui # for serving /api-docsMonorepo / local:
"dependencies": {
"swagger-og": "file:../swagger-og"
}Quick start
1. Config — openapi.config.ts at your project root:
import * as path from 'path';
const projectRoot = path.resolve(__dirname);
export default {
tsconfigPath: './tsconfig.json',
mode: 'controller',
routeFiles: [path.join(projectRoot, 'src/routes/*.ts')],
controllerFiles: [path.join(projectRoot, 'src/controllers/**/*.ts')],
contractFiles: [path.join(projectRoot, 'src/contracts/api/**/*.api.ts')],
typeRoots: [path.join(projectRoot, 'src/types/**/*.types.ts')],
outputFile: path.join(projectRoot, 'dist/openapi.json'),
info: { title: 'My API', version: '1.0.0', description: 'HTTP API contracts.' },
};2. Wire a handler:
import { GetMyAttendanceResponse } from '../contracts/api/timetable/attendance.api';
static getMyAttendance = async (ctx: any) => {
const data = await getUserAttendanceService(...);
const response: GetMyAttendanceResponse = { success: true, data };
ctx.body = response;
};3. Verify & build:
npx swagger-og check --config openapi.config.ts
npx swagger-og check --config openapi.config.ts --strict # fail CI on incorrectly configured types
npm run buildConfiguration: paths and globs
routeFiles, controllerFiles, contractFiles, and typeRoots are string[] glob patterns. Multiple entries are allowed. There is no built-in default folder.
The glob is part of the path string: folder + pattern together.
src/apis/contracts/**/*.api.ts
│ │ │
│ │ └─ file name filter
│ └─ nested folders
└─ your directoryDifferent folder (new service)
If contracts live at src/apis/contracts instead of src/contracts/api:
contractFiles: [
path.join(projectRoot, 'src/apis/contracts/**/*.api.ts'),
],Controllers in that service import from src/apis/contracts and wire const response: GetXResponse. Change controllerFiles / routeFiles the same way if those trees differ. Each property is independent.
Multiple trees at once
contractFiles: [
path.join(projectRoot, 'src/contracts/api/**/*.api.ts'),
path.join(projectRoot, 'src/modules/**/api/*.api.ts'),
path.join(projectRoot, '../shared-api/**/*.api.ts'),
],Putting "/" does not mean “scan every file in the repo.”
These fields are glob patterns, not a start directory. "/" is treated as a path, not a recursive filesystem walk. A directory path without a file pattern (e.g. src/contracts/api) also does not recurse into files inside it. Use ** to include nested folders.
| Value | What it matches |
|-------|-----------------|
| '/' | Not “all files” — not a recursive repo scan |
| 'src/contracts/api' | That directory itself, not files inside it |
| 'src/foo/*.ts' | Files in that folder only (not nested) |
| 'src/foo/**/*.ts' | That folder and all nested folders |
Controller detection
swagger-og looks in the handler body for:
| Signal | Used as |
|--------|---------|
| const response: GetXResponse = ... then ctx.body = response | response (required for visibility) |
| ctx.body = x satisfies GetXResponse | response (optional alternative) |
| const body: MarkXRequestBody = ... | request body |
| parseGetXQuery(...) | query |
| const params: GetXParams = ... | path params |
No wiring → not visible in Swagger. The route still works.
Add an endpoint to Swagger
- Route last argument must be
Controller.methodName. - Define
export type GetXResponse = ...under whatever folder you listed incontractFiles. - Wire it in the controller (
const response: GetXResponse = ...; ctx.body = response). - Run
swagger-og check.
swagger-og check report
| Label | Meaning | In Swagger? |
|-------|---------|-------------|
| Visible | Controller is correctly configured (wired response type) | Yes |
| Not visible | No wired response type | No (OK) |
| Incorrectly configured | Contract *Response exists but controller does not wire it | No — wire or delete |
--strict exits with code 1 only on incorrectly configured items, not on not-visible routes.
Serve docs
import { registerSwaggerRoutes } from 'swagger-og';
if (process.env.SWAGGER_DOCS_ENABLED === 'true') {
registerSwaggerRoutes(router, {
specPath: path.join(__dirname, 'openapi.json'),
specFallbackPath: path.join(process.cwd(), 'dist/openapi.json'),
});
}CLI
swagger-og generate [--config openapi.config.ts]
swagger-og check [--config openapi.config.ts] [--strict]Pipeline
- Parse routes →
Controller.handler - Scan controller handler for a wired response type
- If found, resolve that type from
contractFilesand imports (typeRoots) - Generate JSON Schema
- Write OpenAPI 3.0.3 spec
