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
@openapiJSDoc 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:
- Replace Zod with tsoa validation - Use
@Body,@Query,@Pathdecorators with TypeScript types - Hybrid approach - Keep Zod schemas, call
validateRequest()within controller methods - 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.tsmust be version-controlled or regenerated on build
Neutral
- Build step change: Add
tsoa spec && tsoa routesto 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 |