Files
flyer-crawler.projectium.com/docs/adr/0018-api-documentation-strategy.md
Torben Sorensen 2d2cd52011
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
Massive Dependency Modernization Project
2026-02-13 00:34:22 -08:00

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:

  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:

{
  "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

  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
  • ADR-059 - Dependency Modernization (tsoa migration plan)
  • ADR-003 - Input Validation (Zod schemas)
  • ADR-028 - Response Standardization
  • ADR-001 - Error Handling
  • ADR-016 - Security Hardening
  • ADR-048 - Authentication Strategy