Files
flyer-crawler.projectium.com/docs/adr/0059-dependency-modernization.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

12 KiB

ADR-059: Dependency Modernization Plan

Status: Accepted Date: 2026-02-12 Implemented: 2026-02-12

Context

NPM audit and security scanning identified deprecated dependencies requiring modernization:

Dependency Current Issue Replacement
swagger-jsdoc 6.2.8 Unmaintained since 2022 tsoa (decorator-based OpenAPI)
rimraf 6.1.2 Legacy cleanup utility Node.js fs.rm() (native since v14.14)

Constraints:

  • Existing @openapi JSDoc annotations in 20 route files
  • ADR-018 compliance (API documentation strategy)
  • Zero-downtime migration (phased approach)
  • Must maintain Express 5.x compatibility

Decision

1. swagger-jsdoc → tsoa Migration

Architecture: tsoa controller classes + Express integration (no replacement of Express routing layer).

Current:  Route Files → JSDoc Annotations → swagger-jsdoc → OpenAPI Spec
Future:   Controller Classes → @Route/@Get decorators → tsoa → OpenAPI Spec + Route Registration

Controller Pattern: Base controller providing common utilities:

// src/controllers/base.controller.ts
export abstract class BaseController {
  protected sendSuccess<T>(res: Response, data: T, status = 200) {
    return sendSuccess(res, data, status);
  }
  protected sendError(
    res: Response,
    code: ErrorCode,
    msg: string,
    status: number,
    details?: unknown,
  ) {
    return sendError(res, code, msg, status, details);
  }
}

Express Integration Strategy: tsoa generates routes.ts; wrap with Express middleware pipeline:

// server.ts integration
import { RegisterRoutes } from './src/generated/routes';
RegisterRoutes(app); // tsoa registers routes with existing Express app

2. rimraf → fs.rm() Migration

Change: Replace rimraf coverage .coverage script with Node.js native API.

// package.json (before)
"clean": "rimraf coverage .coverage"

// package.json (after)
"clean": "node -e \"import('fs/promises').then(fs => Promise.all([fs.rm('coverage', {recursive:true,force:true}), fs.rm('.coverage', {recursive:true,force:true})]))\""

Alternative: Create scripts/clean.mjs for maintainability:

// scripts/clean.mjs
import { rm } from 'fs/promises';
await Promise.all([
  rm('coverage', { recursive: true, force: true }),
  rm('.coverage', { recursive: true, force: true }),
]);

Implementation Plan

Phase 1: Infrastructure (Tasks 1-4)

Task Description Dependencies
1 Install tsoa, configure tsoa.json None
2 Create BaseController with utility methods Task 1
3 Configure Express integration (RegisterRoutes) Task 2
4 Set up tsoa spec generation in build pipeline Task 3

Phase 2: Controller Migration (Tasks 5-14)

Priority order matches ADR-018:

Task Route File Controller Class Dependencies
5 health.routes.ts HealthController Task 4
6 auth.routes.ts AuthController Task 4
7 gamification.routes.ts AchievementsController Task 4
8 flyer.routes.ts FlyersController Task 4
9 user.routes.ts UsersController Task 4
10 budget.routes.ts BudgetController Task 4
11 recipe.routes.ts RecipeController Task 4
12 store.routes.ts StoreController Task 4
13 admin.routes.ts AdminController Task 4
14 Remaining routes (deals, price, upc, inventory, ai, receipt, category, stats, personalization, reactions, system) Various Task 4

Phase 3: Cleanup and rimraf (Tasks 15-18)

Task Description Dependencies
15 Create scripts/clean.mjs None
16 Update package.json clean script Task 15
17 Remove rimraf dependency Task 16
18 Remove swagger-jsdoc + types Tasks 5-14 complete

Phase 4: Verification (Tasks 19-24)

Task Description Dependencies
19 Run type-check Tasks 15-18
20 Run unit tests Task 19
21 Run integration tests Task 20
22 Verify OpenAPI spec completeness Task 21
23 Update ADR-018 (reference tsoa) Task 22
24 Update CLAUDE.md (swagger → tsoa) Task 23

Task Dependency Graph

                    [1: Install tsoa]
                           |
                    [2: BaseController]
                           |
                    [3: Express Integration]
                           |
                    [4: Build Pipeline]
                           |
        +------------------+------------------+
        |         |        |        |         |
       [5]       [6]      [7]      [8]    [9-14]
     Health     Auth    Gamif    Flyer   Others
        |         |        |        |         |
        +------------------+------------------+
                           |
                    [18: Remove swagger-jsdoc]
                           |
[15: clean.mjs] -----> [16: Update pkg.json]
                           |
                    [17: Remove rimraf]
                           |
                    [19: type-check]
                           |
                    [20: unit tests]
                           |
                    [21: integration tests]
                           |
                    [22: Verify OpenAPI]
                           |
                    [23: Update ADR-018]
                           |
                    [24: Update CLAUDE.md]

Critical Path

Minimum time to completion: Tasks 1 → 2 → 3 → 4 → 5 (or any controller) → 18 → 19 → 20 → 21 → 22 → 23 → 24

Parallelization opportunities:

  • Tasks 5-14 (all controller migrations) can run in parallel after Task 4
  • Tasks 15-17 (rimraf removal) can run in parallel with controller migrations

Technical Decisions

tsoa Configuration

// tsoa.json
{
  "entryFile": "server.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "controllerPathGlobs": ["src/controllers/**/*.controller.ts"],
  "spec": {
    "outputDirectory": "src/generated",
    "specVersion": 3,
    "basePath": "/api/v1"
  },
  "routes": {
    "routesDir": "src/generated",
    "middleware": "express"
  }
}

Decorator Migration Example

Before (swagger-jsdoc):

/**
 * @openapi
 * /health/ping:
 *   get:
 *     summary: Simple ping endpoint
 *     tags: [Health]
 *     responses:
 *       200:
 *         description: Server is responsive
 */
router.get('/ping', validateRequest(emptySchema), handler);

After (tsoa):

@Route('health')
@Tags('Health')
export class HealthController extends BaseController {
  @Get('ping')
  @SuccessResponse(200, 'Server is responsive')
  public async ping(): Promise<{ message: string }> {
    return { message: 'pong' };
  }
}

Zod Integration

tsoa uses its own validation. Options:

  1. Replace Zod with tsoa validation - Use @Body, @Query, @Path decorators with TypeScript types
  2. Hybrid approach - Keep Zod schemas, call validateRequest() within controller methods
  3. Custom template - Generate tsoa routes that call Zod validation middleware

Recommended: Option 1 for new controllers; gradually migrate existing Zod schemas.

Risk Mitigation

Risk Likelihood Impact Mitigation
tsoa/Express 5.x incompatibility Medium High Test in dev container before migration
Missing OpenAPI coverage post-migration Low Medium Compare generated specs before/after
Authentication middleware integration Medium Medium Test @Security decorator with passport-jwt
Test regression from route changes Low High Run full test suite after each controller
Build time increase (tsoa generation) Low Low Add to npm run build; cache generated files

Consequences

Positive

  • Type-safe API contracts: tsoa decorators derive types from TypeScript
  • Reduced duplication: No more parallel JSDoc + TypeScript type definitions
  • Modern tooling: Active tsoa community (vs. unmaintained swagger-jsdoc)
  • Native Node.js: fs.rm() is built-in, no external dependency
  • Smaller dependency tree: Remove rimraf (5 transitive deps) + swagger-jsdoc (8 transitive deps)

Negative

  • Learning curve: Decorator-based controller pattern differs from Express handlers
  • Migration effort: 20 route files require conversion
  • Generated code: src/generated/routes.ts must be version-controlled or regenerated on build

Neutral

  • Build step change: Add tsoa spec && tsoa routes to build pipeline
  • Testing approach: May need to adjust test structure for controller classes

Alternatives Considered

1. Update swagger-jsdoc to fork/successor

Rejected: No active fork; community has moved to tsoa, fastify-swagger, or NestJS.

2. NestJS migration

Rejected: Full framework migration (Express → NestJS) is disproportionate to the problem scope.

3. fastify-swagger

Rejected: Requires Express → Fastify migration; out of scope.

4. Keep rimraf, accept deprecation warning

Rejected: Native fs.rm() is trivial replacement; no reason to maintain deprecated dependency.

Key Files

File Purpose
tsoa.json tsoa configuration
src/controllers/base.controller.ts Base controller with utilities
src/controllers/*.controller.ts Individual domain controllers
src/generated/routes.ts tsoa-generated Express routes
src/generated/swagger.json Generated OpenAPI 3.0 spec
scripts/clean.mjs Native fs.rm() replacement for rimraf
  • ADR-018 - API Documentation Strategy (will be updated)
  • ADR-003 - Input Validation (Zod integration)
  • ADR-028 - Response Standardization (BaseController pattern)
  • ADR-001 - Error Handling (error utilities in BaseController)