# 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). ```text 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: ```typescript // src/controllers/base.controller.ts export abstract class BaseController { protected sendSuccess(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: ```typescript // 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. ```json // 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: ```javascript // 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 ```text [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 ```json // 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): ```typescript /** * @openapi * /health/ping: * get: * summary: Simple ping endpoint * tags: [Health] * responses: * 200: * description: Server is responsive */ router.get('/ping', validateRequest(emptySchema), handler); ``` **After** (tsoa): ```typescript @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 | ## Related ADRs - [ADR-018](./0018-api-documentation-strategy.md) - API Documentation Strategy (will be updated) - [ADR-003](./0003-standardized-input-validation-using-middleware.md) - Input Validation (Zod integration) - [ADR-028](./0028-api-response-standardization.md) - Response Standardization (BaseController pattern) - [ADR-001](./0001-standardized-error-handling.md) - Error Handling (error utilities in BaseController)