Step 2: Define Controllers with Decorators
Decorators drive schema generation. Types flow directly into the OpenAPI spec.
// src/controllers/user.controller.ts
import { Controller, Get, Post, Route, SuccessResponse, Request, Response, Body, Path, Validate } from 'tsoa';
import { User, CreateUserRequest, ErrorResponse } from '../models';
@Route('users')
export class UserController extends Controller {
@Get('{id}')
@Response<ErrorResponse>('404', 'User not found')
public async getUser(@Path() id: string): Promise<User> {
const user = await this.userService.findById(id);
if (!user) {
this.setStatus(404);
throw new Error('User not found');
}
return user;
}
@Post()
@SuccessResponse('201', 'Created')
@Response<ErrorResponse>('400', 'Validation error')
public async createUser(
@Body() requestBody: CreateUserRequest
): Promise<User> {
this.setStatus(201);
return this.userService.create(requestBody);
}
}
Step 3: Generate OpenAPI Specification
tsoa compiles decorators into a valid OpenAPI 3.0 document. No manual YAML editing required.
# Generates spec and routes
npx tsoa spec
npx tsoa routes
The output includes:
- Path definitions with HTTP methods
- Request/response schemas derived from TypeScript interfaces
- Parameter validation rules
- Security scheme placeholders
- Example payloads (when configured)
Step 4: Serve Documentation
Host the generated swagger.json with Redoc or Swagger UI. Both support dynamic reloading when the spec changes.
// src/server.ts
import express from 'express';
import { RegisterRoutes } from './routes';
import swaggerUi from 'swagger-ui-express';
import * as swaggerDocument from './swagger.json';
const app = express();
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
RegisterRoutes(app);
app.listen(3000);
Step 5: Automate via CI/CD
Documentation must update on every merge. The pipeline validates the spec, generates consumer SDKs, and deploys static docs.
# .github/workflows/api-docs.yml
name: API Documentation Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
generate-and-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx tsoa spec
- run: npx @apidevtools/swagger-cli validate swagger.json
- run: npx swagger-typescript-api -p swagger.json -o ./generated/clients
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public/docs
Architecture Decisions & Rationale
- Code-first over spec-first: Decoupled specs drift. Code-first guarantees alignment between runtime behavior and documentation.
- Decorator-driven generation: Minimal boilerplate. TypeScript types flow directly into JSON Schema without manual mapping.
- CI validation gate: Failing the OpenAPI validation step blocks merges. This enforces contract discipline before deployment.
- Static hosting over dynamic endpoints: Generated docs should be immutable artifacts. Serve from CDN/GitHub Pages for reliability and caching.
- SDK generation in pipeline: Consumers receive typed clients automatically. Reduces integration friction and version mismatch errors.
Pitfall Guide
1. Blind Auto-Generation Without Validation
Problem: Assuming decorator output is production-ready. TypeScript interfaces do not enforce runtime validation. Missing @Validate() or Zod/TypeBox integration results in specs that describe ideal shapes but accept malformed payloads.
Fix: Pair tsoa with runtime validation middleware. Use zod-to-openapi or tsoa's built-in validation to ensure the spec matches actual request handling.
2. Missing Security Scheme Definitions
Problem: Generated specs omit authentication flows. Consumers cannot test endpoints or generate accurate SDKs.
Fix: Explicitly declare @Security('bearerAuth') on controllers and configure securitySchemes in tsoa.json. Never assume consumers will guess the auth method.
3. Skipping Versioning Strategy
Problem: Overwriting swagger.json on every release. Breaking changes silently invalidate existing consumers.
Fix: Version endpoints via path (/v1/users) or header. Generate separate specs per version. Store specs in docs/versions/v1/, docs/versions/v2/. Automate version bumping in release scripts.
4. Leaking Internal Error Structures
Problem: Auto-generated error responses expose stack traces, database queries, or internal enum values.
Fix: Define explicit ErrorResponse interfaces. Strip internal fields before spec generation. Use @Response decorators to control exactly what consumers see.
5. Ignoring Example Generation
Problem: Specs contain schemas but no examples. Consumers must guess payload structures, increasing integration time.
Fix: Configure example generation from TypeScript defaults or Zod schemas. Use swagger-typescript-api with --extract-response-body to pull realistic samples. Validate examples against the spec in CI.
6. Treating Automation as One-Time Setup
Problem: Initial pipeline works, but team stops maintaining decorator consistency. New developers bypass patterns.
Fix: Enforce linting rules (eslint-plugin-tsoa). Add spec validation to pre-commit hooks. Require PR templates that link to generated doc previews. Audit decorator usage quarterly.
7. Over-Reliance on AI for Doc Generation
Problem: Using LLMs to write descriptions or examples without grounding them in the actual schema. AI hallucinates parameters, omits required fields, or misstates rate limits.
Fix: Use AI only for descriptive text augmentation. Anchor all examples and schemas to the generated OpenAPI document. Validate AI output against the spec before publishing.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup MVP (1-2 devs, rapid iteration) | tsoa + GitHub Pages + Swagger UI | Fastest setup, zero infra overhead, auto-syncs with code | <$200/mo (hosting + CI minutes) |
| Enterprise multi-team (3+ services, strict compliance) | Code-first + OpenAPI validation gate + Contract testing (Dredd/Pact) + Versioned spec registry | Enforces contract discipline, prevents cross-team breakage, audit-ready | $1,500β$3,000/mo (CI runners, spec registry, testing infra) |
| Legacy monolith migration | Post-hoc OpenAPI export β gradual decorator adoption β full code-first | Avoids rewrite risk, documents existing endpoints, enables incremental modernization | $800β$2,200/mo (analysis tools, dual-maintenance period) |
Configuration Template
tsoa.json
{
"entryFile": "src/server.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/controllers/**/*.controller.ts"],
"spec": {
"outputDirectory": "public/docs",
"specVersion": 3,
"basePath": "/api",
"securityDefinitions": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
},
"produces": ["application/json"],
"consumes": ["application/json"]
},
"routes": {
"routesDir": "src/routes",
"middleware": "express"
}
}
GitHub Actions Workflow (.github/workflows/api-docs.yml)
name: API Documentation Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx tsoa spec
- run: npx @apidevtools/swagger-cli validate public/docs/swagger.json
- run: npx swagger-typescript-api -p public/docs/swagger.json -o ./generated/clients --axios
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: public/docs/swagger.json
Quick Start Guide
-
Initialize project and install dependencies:
npm init -y
npm install tsoa express @types/express typescript ts-node
npx tsoa --init
-
Create a controller with decorators:
mkdir -p src/controllers
# Paste the UserController example from Core Solution into src/controllers/user.controller.ts
-
Generate spec and routes:
npx tsoa spec
npx tsoa routes
-
Serve documentation locally:
npx serve public/docs -p 4000
# Open http://localhost:4000/swagger.json in Swagger UI or Redoc
-
Validate in CI:
Add the GitHub Actions workflow above. Push to a branch. Verify the pipeline generates the spec, validates it, and blocks merges if decorators are malformed or missing required fields.
Documentation automation transforms API metadata from a maintenance liability into a compile-time guarantee. When the spec is generated deterministically, validated in CI, and versioned alongside releases, integration friction collapses. The engineering investment pays back within one sprint through reduced support overhead, faster consumer onboarding, and elimination of contract drift.