Files
flyer-crawler.projectium.com/docs/adr/0008-api-versioning-strategy.md
Torben Sorensen 45ac4fccf5
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m15s
comprehensive documentation review + test fixes
2026-01-28 16:35:38 -08:00

410 lines
14 KiB
Markdown

# ADR-008: API Versioning Strategy
**Date**: 2025-12-12
**Status**: Accepted (Phase 2 Complete - All Tasks Done)
**Updated**: 2026-01-27
**Completion Note**: Phase 2 fully complete including test path migration. All 23 integration test files updated to use `/api/v1/` paths. Test suite improved from 274/348 to 345/348 passing (3 remain as todo/skipped for known issues unrelated to versioning).
## Context
As the application grows, the API will need to evolve. Making breaking changes to existing endpoints can disrupt clients (e.g., a mobile app or the web frontend). The current routing has no formal versioning scheme.
### Current State
As of January 2026, the API operates without explicit versioning:
- All routes are mounted under `/api/*` (e.g., `/api/flyers`, `/api/users/profile`)
- The frontend `apiClient.ts` uses `API_BASE_URL = '/api'` as the base
- No version prefix exists in route paths
- Breaking changes would immediately affect all consumers
### Why Version Now?
1. **Future Mobile App**: A native mobile app is planned, which will have slower update cycles than the web frontend
2. **Third-Party Integrations**: Store partners may integrate with our API
3. **Deprecation Path**: Need a clear way to deprecate and remove endpoints
4. **Documentation**: OpenAPI documentation (ADR-018) should reflect versioned endpoints
## Decision
We will adopt a URI-based versioning strategy for the API using a phased rollout approach. All routes will be prefixed with a version number (e.g., `/api/v1/flyers`).
### Versioning Format
```text
/api/v{MAJOR}/resource
```
- **MAJOR**: Incremented for breaking changes (v1, v2, v3...)
- Resource paths remain unchanged within a version
### What Constitutes a Breaking Change?
The following changes require a new API version:
| Change Type | Breaking? | Example |
| ----------------------------- | --------- | ------------------------------------------ |
| Remove endpoint | Yes | DELETE `/api/v1/legacy-feature` |
| Remove response field | Yes | Remove `user.email` from response |
| Change response field type | Yes | `id: number` to `id: string` |
| Change required request field | Yes | Make `email` required when it was optional |
| Rename endpoint | Yes | `/users` to `/accounts` |
| Add optional response field | No | Add `user.avatar_url` |
| Add optional request field | No | Add optional `page` parameter |
| Add new endpoint | No | Add `/api/v1/new-feature` |
| Fix bug in behavior | No\* | Correct calculation error |
\*Bug fixes may warrant version increment if clients depend on the buggy behavior.
## Implementation Phases
### Phase 1: Namespace Migration (Current)
**Goal**: Add `/v1/` prefix to all existing routes without behavioral changes.
**Changes Required**:
1. **server.ts**: Update all route registrations
```typescript
// Before
app.use('/api/auth', authRouter);
// After
app.use('/api/v1/auth', authRouter);
```
2. **apiClient.ts**: Update base URL
```typescript
// Before
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
// After
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1';
```
3. **swagger.ts**: Update server definition
```typescript
servers: [
{
url: '/api/v1',
description: 'API v1 server',
},
],
```
4. **Redirect Middleware** (optional): Support legacy clients
```typescript
// Redirect unversioned routes to v1
app.use('/api/:resource', (req, res, next) => {
if (req.params.resource !== 'v1') {
return res.redirect(307, `/api/v1/${req.params.resource}${req.url}`);
}
next();
});
```
**Acceptance Criteria**:
- All existing functionality works at `/api/v1/*`
- Frontend makes requests to `/api/v1/*`
- OpenAPI documentation reflects `/api/v1/*` paths
- Integration tests pass with new paths
### Phase 2: Versioning Infrastructure
**Goal**: Build tooling to support multiple API versions.
**Components**:
1. **Version Router Factory**
```typescript
// src/routes/versioned.ts
export function createVersionedRoutes(version: 'v1' | 'v2') {
const router = express.Router();
if (version === 'v1') {
router.use('/auth', authRouterV1);
router.use('/users', userRouterV1);
// ...
} else if (version === 'v2') {
router.use('/auth', authRouterV2);
router.use('/users', userRouterV2);
// ...
}
return router;
}
```
2. **Version Detection Middleware**
```typescript
// Extract version from URL and attach to request
app.use('/api/:version', (req, res, next) => {
req.apiVersion = req.params.version;
next();
});
```
3. **Deprecation Headers**
```typescript
// Middleware to add deprecation headers
function deprecateVersion(sunsetDate: string) {
return (req, res, next) => {
res.set('Deprecation', 'true');
res.set('Sunset', sunsetDate);
res.set('Link', '</api/v2>; rel="successor-version"');
next();
};
}
```
### Phase 3: Version 2 Support
**Goal**: Introduce v2 API when breaking changes are needed.
**Triggers for v2**:
- Major schema changes (e.g., unified item model)
- Response format overhaul
- Authentication mechanism changes
- Significant performance-driven restructuring
**Parallel Support**:
```typescript
app.use('/api/v1', createVersionedRoutes('v1'));
app.use('/api/v2', createVersionedRoutes('v2'));
```
## Migration Path
### For Frontend (Web)
The web frontend is deployed alongside the API, so migration is straightforward:
1. Update `API_BASE_URL` in `apiClient.ts`
2. Update any hardcoded paths in tests
3. Deploy frontend and backend together
### For External Consumers
External consumers (mobile apps, partner integrations) need a transition period:
1. **Announcement**: 30 days before deprecation of v(N-1)
2. **Deprecation Headers**: Add headers 30 days before sunset
3. **Documentation**: Maintain docs for both versions during transition
4. **Sunset**: Remove v(N-1) after grace period
## Deprecation Timeline
| Version | Status | Sunset Date | Notes |
| -------------------- | ---------- | ---------------------- | --------------- |
| Unversioned `/api/*` | Deprecated | Phase 1 completion | Redirect to v1 |
| v1 | Active | TBD (when v2 releases) | Current version |
### Support Policy
- **Current Version (v(N))**: Full support, all features
- **Previous Version (v(N-1))**: Security fixes only for 6 months after v(N) release
- **Older Versions**: No support, endpoints return 410 Gone
## Backwards Compatibility Strategy
### Redirect Middleware
For a smooth transition, implement redirects from unversioned to versioned endpoints:
```typescript
// src/middleware/versionRedirect.ts
import { Request, Response, NextFunction } from 'express';
import { logger } from '../services/logger.server';
/**
* Middleware to redirect unversioned API requests to v1.
* This provides backwards compatibility during the transition period.
*
* Example: /api/flyers -> /api/v1/flyers (307 Temporary Redirect)
*/
export function versionRedirectMiddleware(req: Request, res: Response, next: NextFunction) {
const path = req.path;
// Skip if already versioned
if (path.startsWith('/v1') || path.startsWith('/v2')) {
return next();
}
// Skip health checks and documentation
if (path.startsWith('/health') || path.startsWith('/docs')) {
return next();
}
// Log deprecation warning
logger.warn(
{
path: req.originalUrl,
method: req.method,
ip: req.ip,
},
'Unversioned API request - redirecting to v1',
);
// Use 307 to preserve HTTP method
const redirectUrl = `/api/v1${path}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`;
return res.redirect(307, redirectUrl);
}
```
### Response Versioning Headers
All API responses include version information:
```typescript
// Middleware to add version headers
app.use('/api/v1', (req, res, next) => {
res.set('X-API-Version', 'v1');
next();
});
```
## Consequences
### Positive
- **Clear Evolution Path**: Establishes a critical pattern for long-term maintainability
- **Client Protection**: Allows the API to evolve without breaking existing clients
- **Parallel Development**: Can develop v2 features while maintaining v1 stability
- **Documentation Clarity**: Each version has its own complete documentation
- **Graceful Deprecation**: Clients have clear timelines and migration paths
### Negative
- **Routing Complexity**: Adds complexity to the routing setup
- **Code Duplication**: May need to maintain multiple versions of handlers
- **Testing Overhead**: Tests may need to cover multiple versions
- **Documentation Maintenance**: Must keep docs for multiple versions in sync
### Mitigation
- Use shared business logic with version-specific adapters
- Automate deprecation header addition
- Generate versioned OpenAPI specs from code
- Clear internal guidelines on when to increment versions
## Key Files
| File | Purpose |
| ----------------------------------- | ------------------------------------------- |
| `server.ts` | Route registration with version prefixes |
| `src/services/apiClient.ts` | Frontend API base URL configuration |
| `src/config/swagger.ts` | OpenAPI server URL and version info |
| `src/routes/*.routes.ts` | Individual route handlers |
| `src/middleware/versionRedirect.ts` | Backwards compatibility redirects (Phase 1) |
## Related ADRs
- [ADR-003](./0003-standardized-input-validation-using-middleware.md) - Input Validation (consistent across versions)
- [ADR-018](./0018-api-documentation-strategy.md) - API Documentation Strategy (versioned OpenAPI specs)
- [ADR-028](./0028-api-response-standardization.md) - Response Standardization (envelope pattern applies to all versions)
- [ADR-016](./0016-api-security-hardening.md) - Security Hardening (applies to all versions)
- [ADR-057](./0057-test-remediation-post-api-versioning.md) - Test Remediation Post-API Versioning (documents test migration)
## Implementation Checklist
### Phase 1 Tasks
- [x] Update `server.ts` to mount all routes under `/api/v1/`
- [x] Update `src/services/apiClient.ts` API_BASE_URL to `/api/v1`
- [x] Update `src/config/swagger.ts` server URL to `/api/v1`
- [x] Add redirect middleware for unversioned requests
- [x] Update integration tests to use versioned paths
- [x] Update API documentation examples (Swagger server URL updated)
- [x] Verify all health checks work at `/api/v1/health/*`
### Phase 2 Tasks
**Implementation Guide**: [API Versioning Infrastructure](../architecture/api-versioning-infrastructure.md)
**Developer Guide**: [API Versioning Developer Guide](../development/API-VERSIONING.md)
- [x] Create version router factory (`src/routes/versioned.ts`)
- [x] Implement deprecation header middleware (`src/middleware/deprecation.middleware.ts`)
- [x] Add version detection to request context (`src/middleware/apiVersion.middleware.ts`)
- [x] Add version types to Express Request (`src/types/express.d.ts`)
- [x] Create version constants configuration (`src/config/apiVersions.ts`)
- [x] Update server.ts to use version router factory
- [x] Update swagger.ts for multi-server documentation
- [x] Add unit tests for version middleware
- [x] Add integration tests for versioned router
- [x] Document versioning patterns for developers
- [x] Migrate all test files to use `/api/v1/` paths (23 files, ~70 occurrences)
### Test Path Migration Summary (2026-01-27)
The final cleanup task for Phase 2 was completed by updating all integration test files to use versioned API paths:
| Metric | Value |
| ---------------------------- | ---------------------------------------- |
| Test files updated | 23 |
| Path occurrences changed | ~70 |
| Test failures resolved | 71 (274 -> 345 passing) |
| Tests remaining todo/skipped | 3 (known issues, not versioning-related) |
| Type check | Passing |
| Versioning-specific tests | 82/82 passing |
**Test Results After Migration**:
- Integration tests: 345/348 passing
- Unit tests: 3,375/3,391 passing (16 pre-existing failures unrelated to versioning)
### Unit Test Path Fix (2026-01-27)
Following the test path migration, 16 unit test failures were discovered and fixed. These failures were caused by error log messages using hardcoded `/api/` paths instead of versioned `/api/v1/` paths.
**Root Cause**: Error log messages in route handlers used hardcoded path strings like:
```typescript
// INCORRECT - hardcoded path doesn't reflect actual request URL
req.log.error({ error }, 'Error in /api/flyers/:id:');
```
**Solution**: Updated to use `req.originalUrl` for dynamic path logging:
```typescript
// CORRECT - uses actual request URL including version prefix
req.log.error({ error }, `Error in ${req.originalUrl.split('?')[0]}:`);
```
**Files Modified**:
| File | Changes |
| -------------------------------------- | ---------------------------------- |
| `src/routes/recipe.routes.ts` | 3 error log statements updated |
| `src/routes/stats.routes.ts` | 1 error log statement updated |
| `src/routes/flyer.routes.ts` | 2 error logs + 2 test expectations |
| `src/routes/personalization.routes.ts` | 3 error log statements updated |
**Test Results After Fix**:
- Unit tests: 3,382/3,391 passing (0 failures in fixed files)
- Remaining 9 failures are pre-existing, unrelated issues (CSS/mocking)
**Best Practice**: See [Error Logging Path Patterns](../development/ERROR-LOGGING-PATHS.md) for guidance on logging request paths in error handlers.
**Migration Documentation**: [Test Path Migration Guide](../development/test-path-migration.md)
### Phase 3 Tasks (Future)
- [ ] Identify breaking changes requiring v2
- [ ] Create v2 route handlers
- [ ] Set deprecation timeline for v1
- [ ] Migrate documentation to multi-version format