17 KiB
ADR-018: API Documentation Strategy
Date: 2025-12-12
Status: Superseded
Superseded By: This ADR was updated in February 2026 to reflect the migration from swagger-jsdoc to tsoa. The original approach using JSDoc annotations has been replaced with a decorator-based controller pattern.
Implemented: 2026-02-12
Context
As the API grows, it becomes increasingly difficult for frontend developers and other consumers to understand its endpoints, request formats, and response structures. There is no single source of truth for API documentation.
Key requirements:
- Developer Experience: Developers need interactive documentation to explore and test API endpoints.
- Code-Documentation Sync: Documentation should stay in sync with the actual code to prevent drift.
- Low Maintenance Overhead: The documentation approach should be "fast and lite" - minimal additional work for developers.
- Security: Documentation should not expose sensitive information in production environments.
- Type Safety: Documentation should be derived from TypeScript types to ensure accuracy.
Why We Migrated from swagger-jsdoc to tsoa
The original implementation used swagger-jsdoc to generate OpenAPI specs from JSDoc comments. This approach had several limitations:
| Issue | Impact |
|---|---|
swagger-jsdoc unmaintained since 2022 |
Security and compatibility risks |
| JSDoc duplication with TypeScript types | Maintenance burden, potential for drift |
| No runtime validation from schema | Validation logic separate from documentation |
| Manual type definitions in comments | Error-prone, no compiler verification |
Decision
We adopt tsoa for API documentation using a decorator-based controller pattern:
- Controller Classes: Use tsoa decorators (
@Route,@Get,@Post,@Security, etc.) on controller classes. - TypeScript-First: OpenAPI specs are generated directly from TypeScript interfaces and types.
- Swagger UI: Continue using
swagger-ui-expressto serve interactive documentation at/docs/api-docs. - Environment Restriction: Only expose the Swagger UI in development and test environments, not production.
- BaseController Pattern: All controllers extend a base class providing response formatting utilities.
Tooling Selection
| Tool | Purpose |
|---|---|
tsoa (6.6.0) |
Generates OpenAPI 3.0 spec from decorators and routes |
swagger-ui-express |
Serves interactive Swagger UI |
Why tsoa over swagger-jsdoc?
- Type-safe contracts: Decorators derive types directly from TypeScript, eliminating duplicate definitions
- Active maintenance: tsoa has an active community and regular releases
- Route generation: tsoa generates Express routes automatically, reducing boilerplate
- Validation integration: Request body types serve as validation contracts
- Reduced duplication: No more parallel JSDoc + TypeScript type definitions
Implementation Details
tsoa Configuration
Located in tsoa.json:
{
"entryFile": "server.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/controllers/**/*.controller.ts"],
"spec": {
"outputDirectory": "src/config",
"specVersion": 3,
"securityDefinitions": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
},
"basePath": "/api",
"specFileBaseName": "tsoa-spec",
"name": "Flyer Crawler API",
"version": "1.0.0"
},
"routes": {
"routesDir": "src/routes",
"basePath": "/api",
"middleware": "express",
"routesFileName": "tsoa-generated.ts",
"esm": true,
"authenticationModule": "src/middleware/tsoaAuthentication.ts"
}
}
Controller Pattern
Each controller extends BaseController and uses tsoa decorators:
import { Route, Tags, Get, Post, Body, Security, SuccessResponse, Response } from 'tsoa';
import {
BaseController,
SuccessResponse as SuccessResponseType,
ErrorResponse,
} from './base.controller';
interface CreateUserRequest {
email: string;
password: string;
full_name?: string;
}
@Route('users')
@Tags('Users')
export class UserController extends BaseController {
/**
* Create a new user account.
* @summary Create user
* @param requestBody User creation data
* @returns Created user profile
*/
@Post()
@SuccessResponse(201, 'User created')
@Response<ErrorResponse>(400, 'Validation error')
@Response<ErrorResponse>(409, 'Email already exists')
public async createUser(
@Body() requestBody: CreateUserRequest,
): Promise<SuccessResponseType<UserProfileDto>> {
// Implementation
const user = await userService.createUser(requestBody);
return this.created(user);
}
/**
* Get current user's profile.
* @summary Get my profile
* @param request Express request with authenticated user
* @returns User profile
*/
@Get('me')
@Security('bearerAuth')
@SuccessResponse(200, 'Profile retrieved')
@Response<ErrorResponse>(401, 'Not authenticated')
public async getMyProfile(
@Request() request: Express.Request,
): Promise<SuccessResponseType<UserProfileDto>> {
const user = request.user as UserProfile;
return this.success(toUserProfileDto(user));
}
}
BaseController Helpers
The BaseController class provides standardized response formatting:
export abstract class BaseController extends Controller {
// Success response with data
protected success<T>(data: T): SuccessResponse<T> {
return { success: true, data };
}
// Success with 201 Created status
protected created<T>(data: T): SuccessResponse<T> {
this.setStatus(201);
return this.success(data);
}
// Paginated response with metadata
protected paginated<T>(data: T[], pagination: PaginationInput): PaginatedResponse<T> {
return {
success: true,
data,
meta: { pagination: this.calculatePagination(pagination) },
};
}
// Message-only response
protected message(message: string): SuccessResponse<{ message: string }> {
return this.success({ message });
}
// No content response (204)
protected noContent(): void {
this.setStatus(204);
}
// Error response (prefer throwing errors instead)
protected error(code: string, message: string, details?: unknown): ErrorResponse {
return { success: false, error: { code, message, details } };
}
}
Authentication with @Security
tsoa integrates with the existing passport-jwt strategy via a custom authentication module:
// src/middleware/tsoaAuthentication.ts
export async function expressAuthentication(
request: Request,
securityName: string,
_scopes?: string[],
): Promise<UserProfile> {
if (securityName !== 'bearerAuth') {
throw new AuthenticationError(`Unknown security scheme: ${securityName}`);
}
const token = extractBearerToken(request);
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
const userProfile = await userRepo.findUserProfileById(decoded.user_id);
if (!userProfile) {
throw new AuthenticationError('User not found');
}
request.user = userProfile;
return userProfile;
}
Usage in controllers:
@Get('profile')
@Security('bearerAuth')
public async getProfile(@Request() req: Express.Request): Promise<...> {
const user = req.user as UserProfile;
// ...
}
DTO Organization
Shared DTOs are defined in src/dtos/common.dto.ts to avoid duplicate type definitions across controllers:
// src/dtos/common.dto.ts
/**
* Address with flattened coordinates (tsoa-compatible).
* GeoJSONPoint uses coordinates: [number, number] which tsoa cannot handle.
*/
export interface AddressDto {
address_id: number;
address_line_1: string;
city: string;
province_state: string;
postal_code: string;
country: string;
latitude?: number | null; // Flattened from GeoJSONPoint
longitude?: number | null; // Flattened from GeoJSONPoint
// ...
}
export interface UserDto {
user_id: string;
email: string;
created_at: string;
updated_at: string;
}
export interface UserProfileDto {
full_name?: string | null;
role: 'admin' | 'user';
points: number;
user: UserDto;
address?: AddressDto | null;
// ...
}
Swagger UI Setup
In server.ts, the Swagger UI middleware serves the tsoa-generated spec:
import swaggerUi from 'swagger-ui-express';
import tsoaSpec from './src/config/tsoa-spec.json' with { type: 'json' };
// Only serve Swagger UI in non-production environments
if (process.env.NODE_ENV !== 'production') {
app.use('/docs/api-docs', swaggerUi.serve, swaggerUi.setup(tsoaSpec));
// Raw JSON spec for tooling
app.get('/docs/api-docs.json', (_req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(tsoaSpec);
});
}
Build Integration
tsoa spec and route generation is integrated into the build pipeline:
{
"scripts": {
"tsoa:spec": "tsoa spec",
"tsoa:routes": "tsoa routes",
"prebuild": "npm run tsoa:spec && npm run tsoa:routes",
"build": "tsc"
}
}
Response Schema Standardization
All API responses follow the standardized format from ADR-028:
// Success response
{
"success": true,
"data": { ... }
}
// Paginated response
{
"success": true,
"data": [...],
"meta": {
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": false
}
}
}
// Error response
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "User not found"
}
}
API Route Tags
Organize endpoints using consistent tags:
| Tag | Description | Route Prefix |
|---|---|---|
| Health | Server health and readiness checks | /api/health/* |
| Auth | Authentication and authorization | /api/auth/* |
| Users | User profile management | /api/users/* |
| Flyers | Flyer uploads and retrieval | /api/flyers/* |
| Deals | Deal search and management | /api/deals/* |
| Stores | Store information | /api/stores/* |
| Recipes | Recipe management | /api/recipes/* |
| Budgets | Budget tracking | /api/budgets/* |
| Inventory | User inventory management | /api/inventory/* |
| Gamification | Achievements and leaderboards | /api/achievements/* |
| Admin | Administrative operations | /api/admin/* |
| System | System status and monitoring | /api/system/* |
Controller Inventory
The following controllers have been migrated to tsoa:
| Controller | Endpoints | Description |
|---|---|---|
health.controller.ts |
10 | Health checks, probes, service status |
auth.controller.ts |
8 | Login, register, password reset, OAuth |
user.controller.ts |
30 | User profiles, preferences, notifications |
admin.controller.ts |
32 | System administration, user management |
ai.controller.ts |
15 | AI-powered extraction and analysis |
flyer.controller.ts |
12 | Flyer upload and management |
store.controller.ts |
8 | Store information |
recipe.controller.ts |
10 | Recipe CRUD and suggestions |
upc.controller.ts |
6 | UPC barcode lookups |
inventory.controller.ts |
8 | User inventory management |
receipt.controller.ts |
6 | Receipt processing |
budget.controller.ts |
8 | Budget tracking |
category.controller.ts |
4 | Category management |
deals.controller.ts |
8 | Deal search and discovery |
stats.controller.ts |
6 | Usage statistics |
price.controller.ts |
6 | Price history and tracking |
system.controller.ts |
4 | System status |
gamification.controller.ts |
10 | Achievements, leaderboards |
personalization.controller.ts |
6 | User recommendations |
reactions.controller.ts |
4 | Item reactions and ratings |
Security Considerations
- Production Disabled: Swagger UI is not available in production to prevent information disclosure.
- No Sensitive Data: Never include actual secrets, tokens, or PII in example values.
- Authentication Documented: Clearly document which endpoints require authentication.
- Rate Limiting: Rate limiters are applied via
@Middlewaresdecorator.
Testing
Verify API documentation is correct by:
- Manual Review: Navigate to
/docs/api-docsand test each endpoint. - Spec Validation: Use OpenAPI validators to check the generated spec.
- Controller Tests: Each controller has comprehensive test coverage (369 controller tests total).
- Integration Tests: 345 integration tests verify endpoint behavior.
Consequences
Positive
- Type-safe API contracts: tsoa decorators derive types from TypeScript, eliminating duplicate definitions
- Single Source of Truth: Documentation lives with the code and stays in sync
- Active Maintenance: tsoa is actively maintained with regular releases
- Interactive Exploration: Developers can try endpoints directly from Swagger UI
- SDK Generation: OpenAPI spec enables automatic client SDK generation
- Reduced Boilerplate: tsoa generates Express routes automatically
Negative
- Learning Curve: Decorator-based controller pattern differs from Express handlers
- Generated Code:
tsoa-generated.tsmust be regenerated when controllers change - Build Step: Adds
tsoa spec && tsoa routesto the build pipeline
Mitigation
- Migration Guide: Created comprehensive TSOA-MIGRATION-GUIDE.md for developers
- BaseController: Provides familiar response helpers matching existing patterns
- Incremental Adoption: Existing Express routes continue to work alongside tsoa controllers
Key Files
| File | Purpose |
|---|---|
tsoa.json |
tsoa configuration |
src/controllers/base.controller.ts |
Base controller with response utilities |
src/controllers/types.ts |
Shared controller type definitions |
src/controllers/*.controller.ts |
Individual domain controllers |
src/dtos/common.dto.ts |
Shared DTO definitions |
src/middleware/tsoaAuthentication.ts |
JWT authentication handler |
src/routes/tsoa-generated.ts |
tsoa-generated Express routes |
src/config/tsoa-spec.json |
Generated OpenAPI 3.0 spec |
server.ts |
Swagger UI middleware setup |
Migration History
| Date | Change |
|---|---|
| 2025-12-12 | Initial ADR created with swagger-jsdoc approach |
| 2026-01-11 | Began implementation with swagger-jsdoc |
| 2026-02-12 | Completed migration to tsoa, superseding swagger-jsdoc approach |