# 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: 1. **Developer Experience**: Developers need interactive documentation to explore and test API endpoints. 2. **Code-Documentation Sync**: Documentation should stay in sync with the actual code to prevent drift. 3. **Low Maintenance Overhead**: The documentation approach should be "fast and lite" - minimal additional work for developers. 4. **Security**: Documentation should not expose sensitive information in production environments. 5. **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: 1. **Controller Classes**: Use tsoa decorators (`@Route`, `@Get`, `@Post`, `@Security`, etc.) on controller classes. 2. **TypeScript-First**: OpenAPI specs are generated directly from TypeScript interfaces and types. 3. **Swagger UI**: Continue using `swagger-ui-express` to serve interactive documentation at `/docs/api-docs`. 4. **Environment Restriction**: Only expose the Swagger UI in development and test environments, not production. 5. **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`: ```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: ```typescript 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(400, 'Validation error') @Response(409, 'Email already exists') public async createUser( @Body() requestBody: CreateUserRequest, ): Promise> { // 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(401, 'Not authenticated') public async getMyProfile( @Request() request: Express.Request, ): Promise> { const user = request.user as UserProfile; return this.success(toUserProfileDto(user)); } } ``` ### BaseController Helpers The `BaseController` class provides standardized response formatting: ```typescript export abstract class BaseController extends Controller { // Success response with data protected success(data: T): SuccessResponse { return { success: true, data }; } // Success with 201 Created status protected created(data: T): SuccessResponse { this.setStatus(201); return this.success(data); } // Paginated response with metadata protected paginated(data: T[], pagination: PaginationInput): PaginatedResponse { 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: ```typescript // src/middleware/tsoaAuthentication.ts export async function expressAuthentication( request: Request, securityName: string, _scopes?: string[], ): Promise { 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: ```typescript @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: ```typescript // 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: ```typescript 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: ```json { "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](./0028-api-response-standardization.md): ```typescript // 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 1. **Production Disabled**: Swagger UI is not available in production to prevent information disclosure. 2. **No Sensitive Data**: Never include actual secrets, tokens, or PII in example values. 3. **Authentication Documented**: Clearly document which endpoints require authentication. 4. **Rate Limiting**: Rate limiters are applied via `@Middlewares` decorator. ## Testing Verify API documentation is correct by: 1. **Manual Review**: Navigate to `/docs/api-docs` and test each endpoint. 2. **Spec Validation**: Use OpenAPI validators to check the generated spec. 3. **Controller Tests**: Each controller has comprehensive test coverage (369 controller tests total). 4. **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.ts` must be regenerated when controllers change - **Build Step**: Adds `tsoa spec && tsoa routes` to 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 | ## Related ADRs - [ADR-059](./0059-dependency-modernization.md) - Dependency Modernization (tsoa migration plan) - [ADR-003](./0003-standardized-input-validation-using-middleware.md) - Input Validation (Zod schemas) - [ADR-028](./0028-api-response-standardization.md) - Response Standardization - [ADR-001](./0001-standardized-error-handling.md) - Error Handling - [ADR-016](./0016-api-security-hardening.md) - Security Hardening - [ADR-048](./0048-authentication-strategy.md) - Authentication Strategy