12 KiB
Phase 3: API Migration
Overview
This phase updates the vehicles API controller to use the new MVP Platform database for all dropdown endpoints while maintaining exact API compatibility. All existing response formats and authentication patterns are preserved.
Prerequisites
- Phase 2 backend migration completed successfully
- VIN decoder service functional
- MVP Platform repository working correctly
- Backend service can query MVP Platform database
- All TypeScript compilation successful
Current API Endpoints to Update
Existing endpoints that will be updated:
GET /api/vehicles/dropdown/makes(unauthenticated)GET /api/vehicles/dropdown/models/:make(unauthenticated)GET /api/vehicles/dropdown/transmissions(unauthenticated)GET /api/vehicles/dropdown/engines(unauthenticated)GET /api/vehicles/dropdown/trims(unauthenticated)
Existing endpoints that remain unchanged:
POST /api/vehicles(authenticated - uses VIN decoder)GET /api/vehicles(authenticated)GET /api/vehicles/:id(authenticated)PUT /api/vehicles/:id(authenticated)DELETE /api/vehicles/:id(authenticated)
Tasks
Task 3.1: Update Vehicles Controller
Location: backend/src/features/vehicles/api/vehicles.controller.ts
Action: Replace external API dropdown methods with MVP Platform database calls:
// UPDATE imports - REMOVE:
// import { vpicClient } from '../external/vpic/vpic.client';
// ADD new imports:
import { VehiclesService } from '../domain/vehicles.service';
export class VehiclesController {
private vehiclesService: VehiclesService;
constructor() {
this.vehiclesService = new VehiclesService();
}
// UPDATE existing dropdown methods:
async getDropdownMakes(request: FastifyRequest, reply: FastifyReply) {
try {
logger.info('Getting dropdown makes from MVP Platform');
const makes = await this.vehiclesService.getDropdownMakes();
// Maintain exact same response format
const response = makes.map(make => ({
Make_ID: make.id,
Make_Name: make.name
}));
reply.status(200).send(response);
} catch (error) {
logger.error('Get dropdown makes failed', { error });
reply.status(500).send({ error: 'Failed to retrieve makes' });
}
}
async getDropdownModels(request: FastifyRequest<{ Params: { make: string } }>, reply: FastifyReply) {
try {
const { make } = request.params;
logger.info('Getting dropdown models from MVP Platform', { make });
const models = await this.vehiclesService.getDropdownModels(make);
// Maintain exact same response format
const response = models.map(model => ({
Model_ID: model.id,
Model_Name: model.name
}));
reply.status(200).send(response);
} catch (error) {
logger.error('Get dropdown models failed', { error });
reply.status(500).send({ error: 'Failed to retrieve models' });
}
}
async getDropdownTransmissions(request: FastifyRequest, reply: FastifyReply) {
try {
logger.info('Getting dropdown transmissions from MVP Platform');
const transmissions = await this.vehiclesService.getDropdownTransmissions();
// Maintain exact same response format
const response = transmissions.map(transmission => ({
Name: transmission.name
}));
reply.status(200).send(response);
} catch (error) {
logger.error('Get dropdown transmissions failed', { error });
reply.status(500).send({ error: 'Failed to retrieve transmissions' });
}
}
async getDropdownEngines(request: FastifyRequest, reply: FastifyReply) {
try {
logger.info('Getting dropdown engines from MVP Platform');
const engines = await this.vehiclesService.getDropdownEngines();
// Maintain exact same response format
const response = engines.map(engine => ({
Name: engine.name
}));
reply.status(200).send(response);
} catch (error) {
logger.error('Get dropdown engines failed', { error });
reply.status(500).send({ error: 'Failed to retrieve engines' });
}
}
async getDropdownTrims(request: FastifyRequest, reply: FastifyReply) {
try {
logger.info('Getting dropdown trims from MVP Platform');
const trims = await this.vehiclesService.getDropdownTrims();
// Maintain exact same response format
const response = trims.map(trim => ({
Name: trim.name
}));
reply.status(200).send(response);
} catch (error) {
logger.error('Get dropdown trims failed', { error });
reply.status(500).send({ error: 'Failed to retrieve trims' });
}
}
// All other methods remain unchanged (createVehicle, getUserVehicles, etc.)
}
Task 3.2: Verify Routes Configuration
Location: backend/src/features/vehicles/api/vehicles.routes.ts
Action: Ensure dropdown routes remain unauthenticated (no changes needed, just verification):
// VERIFY these routes remain unauthenticated:
fastify.get('/vehicles/dropdown/makes', {
handler: vehiclesController.getDropdownMakes.bind(vehiclesController)
});
fastify.get<{ Params: { make: string } }>('/vehicles/dropdown/models/:make', {
handler: vehiclesController.getDropdownModels.bind(vehiclesController)
});
fastify.get('/vehicles/dropdown/transmissions', {
handler: vehiclesController.getDropdownTransmissions.bind(vehiclesController)
});
fastify.get('/vehicles/dropdown/engines', {
handler: vehiclesController.getDropdownEngines.bind(vehiclesController)
});
fastify.get('/vehicles/dropdown/trims', {
handler: vehiclesController.getDropdownTrims.bind(vehiclesController)
});
Note: These routes should NOT have preHandler: fastify.authenticate to maintain unauthenticated access as required by security.md.
Task 3.3: Update Response Error Handling
Action: Add specific error handling for database connectivity issues:
// Add to VehiclesController class:
private handleDatabaseError(error: any, operation: string, reply: FastifyReply) {
logger.error(`${operation} database error`, { error });
// Check for specific database connection errors
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
reply.status(503).send({
error: 'Service temporarily unavailable',
message: 'Database connection issue'
});
return;
}
// Generic database error
if (error.code && error.code.startsWith('P')) { // PostgreSQL error codes
reply.status(500).send({
error: 'Database query failed',
message: 'Please try again later'
});
return;
}
// Generic error
reply.status(500).send({
error: `Failed to ${operation}`,
message: 'Internal server error'
});
}
// Update all dropdown methods to use this error handler:
// Replace each catch block with:
} catch (error) {
this.handleDatabaseError(error, 'retrieve makes', reply);
}
Task 3.4: Add Performance Monitoring
Action: Add response time logging for performance monitoring:
// Add to VehiclesController class:
private async measurePerformance<T>(
operation: string,
fn: () => Promise<T>
): Promise<T> {
const startTime = Date.now();
try {
const result = await fn();
const duration = Date.now() - startTime;
logger.info(`MVP Platform ${operation} completed`, { duration });
return result;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`MVP Platform ${operation} failed`, { duration, error });
throw error;
}
}
// Update dropdown methods to use performance monitoring:
async getDropdownMakes(request: FastifyRequest, reply: FastifyReply) {
try {
logger.info('Getting dropdown makes from MVP Platform');
const makes = await this.measurePerformance('makes query', () =>
this.vehiclesService.getDropdownMakes()
);
// ... rest of method unchanged
} catch (error) {
this.handleDatabaseError(error, 'retrieve makes', reply);
}
}
Task 3.5: Update Health Check
Location: backend/src/features/vehicles/api/vehicles.controller.ts
Action: Add MVP Platform database health check method:
// Add new health check method:
async healthCheck(request: FastifyRequest, reply: FastifyReply) {
try {
// Test MVP Platform database connection
await this.measurePerformance('health check', async () => {
const testResult = await this.vehiclesService.testMvpPlatformConnection();
if (!testResult) {
throw new Error('MVP Platform database connection failed');
}
});
reply.status(200).send({
status: 'healthy',
mvpPlatform: 'connected',
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Health check failed', { error });
reply.status(503).send({
status: 'unhealthy',
error: error.message,
timestamp: new Date().toISOString()
});
}
}
Location: backend/src/features/vehicles/domain/vehicles.service.ts
Action: Add health check method to service:
// Add to VehiclesService class:
async testMvpPlatformConnection(): Promise<boolean> {
try {
await mvpPlatformRepository.getMakes();
return true;
} catch (error) {
logger.error('MVP Platform connection test failed', { error });
return false;
}
}
Task 3.6: Update Route Registration for Health Check
Location: backend/src/features/vehicles/api/vehicles.routes.ts
Action: Add health check route:
// Add health check route (unauthenticated for monitoring):
fastify.get('/vehicles/health', {
handler: vehiclesController.healthCheck.bind(vehiclesController)
});
Validation Steps
Step 1: Test API Response Formats
# Test makes endpoint
curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '.[0]'
# Should return: {"Make_ID": number, "Make_Name": "string"}
# Test models endpoint
curl -s "http://localhost:3001/api/vehicles/dropdown/models/Honda" | jq '.[0]'
# Should return: {"Model_ID": number, "Model_Name": "string"}
# Test transmissions endpoint
curl -s http://localhost:3001/api/vehicles/dropdown/transmissions | jq '.[0]'
# Should return: {"Name": "string"}
Step 2: Test Performance
# Test response times (should be < 100ms)
time curl -s http://localhost:3001/api/vehicles/dropdown/makes > /dev/null
# Load test with multiple concurrent requests
for i in {1..10}; do
curl -s http://localhost:3001/api/vehicles/dropdown/makes > /dev/null &
done
wait
Step 3: Test Error Handling
# Test with invalid make name
curl -s "http://localhost:3001/api/vehicles/dropdown/models/InvalidMake" | jq '.'
# Should return empty array or appropriate error
# Test health check
curl -s http://localhost:3001/api/vehicles/health | jq '.'
# Should return: {"status": "healthy", "mvpPlatform": "connected", "timestamp": "..."}
Step 4: Verify Authentication Patterns
# Test that dropdown endpoints are unauthenticated (should work without token)
curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '. | length'
# Should return number > 0
# Test that vehicle CRUD endpoints still require authentication
curl -s http://localhost:3001/api/vehicles
# Should return 401 Unauthorized
Error Handling
Common Issues and Solutions
Issue: Empty response arrays Solution: Check MVP Platform database has data, verify SQL queries, check table names
Issue: Slow response times (> 100ms) Solution: Add database indexes, optimize queries, check connection pool settings
Issue: Authentication errors on dropdown endpoints Solution: Verify routes don't have authentication middleware, check security.md compliance
Issue: Wrong response format Solution: Compare with original vPIC API responses, adjust mapping in controller
Rollback Procedure
-
Revert vehicles.controller.ts:
git checkout HEAD -- backend/src/features/vehicles/api/vehicles.controller.ts -
Revert vehicles.routes.ts if modified:
git checkout HEAD -- backend/src/features/vehicles/api/vehicles.routes.ts -
Restart backend service:
docker-compose restart backend
Next Steps
After successful completion of Phase 3:
- Proceed to Phase 4: Scheduled ETL
- Monitor API response times in production
- Set up alerts for health check failures
Dependencies for Next Phase
- All dropdown APIs returning correct data
- Response times consistently under 100ms
- Health check endpoint functional
- No authentication issues with dropdown endpoints
- Error handling working properly