25 KiB
Backend Vehicles API Update - Agent 4
Task: Update Vehicles API to use string-based parameters and responses
Status: Ready for Implementation Dependencies: Agent 3 (Platform Service) must be complete Estimated Time: 1-2 hours Assigned To: Agent 4 (Vehicles API)
Overview
Update the vehicles controller and service to accept string parameters (make, model, trim) instead of IDs, and return string arrays instead of objects. This is the final backend change before frontend updates.
Prerequisites
Verify Agent 3 Completed
# Verify service compiles
cd backend && npm run build
# Should see no errors in vehicle-data.service.ts
Files to Modify
backend/src/features/vehicles/api/vehicles.controller.ts
backend/src/features/vehicles/domain/vehicles.service.ts
backend/src/features/vehicles/types/index.ts (query parameter types)
Current Implementation Analysis
Controller
File: backend/src/features/vehicles/api/vehicles.controller.ts
Current Query Parameters (ID-based):
getDropdownModels: { year: number; make_id: number }
getDropdownTrims: { year: number; make_id: number; model_id: number }
getDropdownEngines: { year: number; make_id: number; model_id: number; trim_id: number }
getDropdownTransmissions: { year: number; make_id: number; model_id: number }
Service
File: backend/src/features/vehicles/domain/vehicles.service.ts
Current Methods:
getDropdownMakes(year: number): Promise<{ id: number; name: string }[]>
getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]>
getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ id: number; name: string }[]>
getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ id: number; name: string }[]>
getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ id: number; name: string }[]> // Hardcoded!
getDropdownYears(): Promise<number[]>
Part 1: Update Vehicles Service
Step 1: Update getDropdownMakes()
Current (lines 165-171):
async getDropdownMakes(year: number): Promise<{ id: number; name: string }[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown makes via platform module', { year });
return vehicleDataService.getMakes(pool, year);
}
New:
async getDropdownMakes(year: number): Promise<string[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown makes via platform module', { year });
return vehicleDataService.getMakes(pool, year);
}
Changes:
- Line 165: Return type
Promise<{ id: number; name: string }[]>→Promise<string[]> - Logic unchanged (platform service now returns string[])
Step 2: Update getDropdownModels()
Current (lines 173-179):
async getDropdownModels(year: number, makeId: number): Promise<{ id: number; name: string }[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown models via platform module', { year, makeId });
return vehicleDataService.getModels(pool, year, makeId);
}
New:
async getDropdownModels(year: number, make: string): Promise<string[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown models via platform module', { year, make });
return vehicleDataService.getModels(pool, year, make);
}
Changes:
- Line 173: Parameter
makeId: number→make: string - Line 173: Return type changed to
Promise<string[]> - Line 177: Logger uses
makeinstead ofmakeId - Line 178: Service call uses
makeinstead ofmakeId
Step 3: Update getDropdownTrims()
Current (lines 197-203):
async getDropdownTrims(year: number, makeId: number, modelId: number): Promise<{ id: number; name: string }[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown trims via platform module', { year, makeId, modelId });
return vehicleDataService.getTrims(pool, year, modelId);
}
New:
async getDropdownTrims(year: number, make: string, model: string): Promise<string[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown trims via platform module', { year, make, model });
return vehicleDataService.getTrims(pool, year, make, model);
}
Changes:
- Line 197: Parameters
makeId: number, modelId: number→make: string, model: string - Line 197: Return type changed to
Promise<string[]> - Line 201: Logger uses
make, modelinstead ofmakeId, modelId - Line 202: Service call uses
make, modelinstead ofmodelId
Step 4: Update getDropdownEngines()
Current (lines 189-195):
async getDropdownEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<{ id: number; name: string }[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown engines via platform module', { year, makeId, modelId, trimId });
return vehicleDataService.getEngines(pool, year, modelId, trimId);
}
New:
async getDropdownEngines(year: number, make: string, model: string, trim: string): Promise<string[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown engines via platform module', { year, make, model, trim });
return vehicleDataService.getEngines(pool, year, make, model, trim);
}
Changes:
- Line 189: Parameters changed to strings:
make: string, model: string, trim: string - Line 189: Return type changed to
Promise<string[]> - Line 193: Logger uses string names
- Line 194: Service call uses all string parameters
Step 5: Update getDropdownTransmissions() - CRITICAL
Current (lines 181-187):
async getDropdownTransmissions(_year: number, _makeId: number, _modelId: number): Promise<{ id: number; name: string }[]> {
logger.info('Providing dropdown transmissions from static list');
return [
{ id: 1, name: 'Automatic' },
{ id: 2, name: 'Manual' }
];
}
New:
async getDropdownTransmissions(year: number, make: string, model: string): Promise<string[]> {
const vehicleDataService = getVehicleDataService();
const pool = getPool();
logger.info('Fetching dropdown transmissions via platform module', { year, make, model });
return vehicleDataService.getTransmissions(pool, year, make, model);
}
Changes:
- Line 181: Parameters changed from IDs to strings:
make: string, model: string - Line 181: Return type changed to
Promise<string[]> - Line 182-186: REMOVED hardcoded static list
- Now calls platform service for real transmission data
Part 2: Update Vehicles Controller
Step 6: Update getDropdownMakes()
Current (lines 153-172):
async getDropdownMakes(request: FastifyRequest<{ Querystring: { year: number } }>, reply: FastifyReply) {
try {
const { year } = request.query;
if (!year || year < 1980 || year > new Date().getFullYear() + 1) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year parameter is required (1980-' + (new Date().getFullYear() + 1) + ')'
});
}
const makes = await this.vehiclesService.getDropdownMakes(year);
return reply.code(200).send(makes);
} catch (error) {
logger.error('Error getting dropdown makes', { error, year: request.query?.year });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get makes'
});
}
}
New: (Unchanged - already returns array, validation unchanged)
Note: No changes needed - query parameter is already just year, and service now returns string[] which gets passed through directly.
Step 7: Update getDropdownModels()
Current (lines 174-193):
async getDropdownModels(request: FastifyRequest<{ Querystring: { year: number; make_id: number } }>, reply: FastifyReply) {
try {
const { year, make_id } = request.query;
if (!year || !make_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year and make_id parameters are required'
});
}
const models = await this.vehiclesService.getDropdownModels(year, make_id);
return reply.code(200).send(models);
} catch (error) {
logger.error('Error getting dropdown models', { error, year: request.query?.year, make_id: request.query?.make_id });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get models'
});
}
}
New:
async getDropdownModels(request: FastifyRequest<{ Querystring: { year: number; make: string } }>, reply: FastifyReply) {
try {
const { year, make } = request.query;
if (!year || !make || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year and make parameters are required'
});
}
const models = await this.vehiclesService.getDropdownModels(year, make);
return reply.code(200).send(models);
} catch (error) {
logger.error('Error getting dropdown models', { error, year: request.query?.year, make: request.query?.make });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get models'
});
}
}
Changes:
- Line 174: Querystring type
make_id: number→make: string - Line 176: Destructure
makeinstead ofmake_id - Line 177: Validation changed from
make_id < 1tomake.trim().length === 0 - Line 180: Error message updated
- Line 184: Service call uses
makestring - Line 187: Logger uses
make
Step 8: Update getDropdownTrims()
Current (lines 237-256):
async getDropdownTrims(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number } }>, reply: FastifyReply) {
try {
const { year, make_id, model_id } = request.query;
if (!year || !make_id || !model_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year, make_id, and model_id parameters are required'
});
}
const trims = await this.vehiclesService.getDropdownTrims(year, make_id, model_id);
return reply.code(200).send(trims);
} catch (error) {
logger.error('Error getting dropdown trims', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get trims'
});
}
}
New:
async getDropdownTrims(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string } }>, reply: FastifyReply) {
try {
const { year, make, model } = request.query;
if (!year || !make || !model || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year, make, and model parameters are required'
});
}
const trims = await this.vehiclesService.getDropdownTrims(year, make, model);
return reply.code(200).send(trims);
} catch (error) {
logger.error('Error getting dropdown trims', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get trims'
});
}
}
Changes:
- Line 237: Querystring types changed to
make: string; model: string - Line 239: Destructure string parameters
- Line 240: Validation uses
.trim().length === 0instead of< 1 - Line 243: Error message updated
- Line 247: Service call uses strings
- Line 250: Logger uses strings
Step 9: Update getDropdownEngines()
Current (lines 216-235):
async getDropdownEngines(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number; trim_id: number } }>, reply: FastifyReply) {
try {
const { year, make_id, model_id, trim_id } = request.query;
if (!year || !make_id || !model_id || !trim_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1 || trim_id < 1) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year, make_id, model_id, and trim_id parameters are required'
});
}
const engines = await this.vehiclesService.getDropdownEngines(year, make_id, model_id, trim_id);
return reply.code(200).send(engines);
} catch (error) {
logger.error('Error getting dropdown engines', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id, trim_id: request.query?.trim_id });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get engines'
});
}
}
New:
async getDropdownEngines(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string } }>, reply: FastifyReply) {
try {
const { year, make, model, trim } = request.query;
if (!year || !make || !model || !trim || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year, make, model, and trim parameters are required'
});
}
const engines = await this.vehiclesService.getDropdownEngines(year, make, model, trim);
return reply.code(200).send(engines);
} catch (error) {
logger.error('Error getting dropdown engines', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model, trim: request.query?.trim });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get engines'
});
}
}
Changes:
- Line 216: Querystring types changed to strings
- Line 218: Destructure string parameters
- Line 219: Validation uses
.trim().length === 0 - Line 222: Error message updated
- Line 226: Service call uses strings
- Line 229: Logger uses strings
Step 10: Update getDropdownTransmissions()
Current (lines 195-214):
async getDropdownTransmissions(request: FastifyRequest<{ Querystring: { year: number; make_id: number; model_id: number } }>, reply: FastifyReply) {
try {
const { year, make_id, model_id } = request.query;
if (!year || !make_id || !model_id || year < 1980 || year > new Date().getFullYear() + 1 || make_id < 1 || model_id < 1) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year, make_id, and model_id parameters are required'
});
}
const transmissions = await this.vehiclesService.getDropdownTransmissions(year, make_id, model_id);
return reply.code(200).send(transmissions);
} catch (error) {
logger.error('Error getting dropdown transmissions', { error, year: request.query?.year, make_id: request.query?.make_id, model_id: request.query?.model_id });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get transmissions'
});
}
}
New:
async getDropdownTransmissions(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string } }>, reply: FastifyReply) {
try {
const { year, make, model } = request.query;
if (!year || !make || !model || year < 1980 || year > new Date().getFullYear() + 1 || make.trim().length === 0 || model.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: 'Valid year, make, and model parameters are required'
});
}
const transmissions = await this.vehiclesService.getDropdownTransmissions(year, make, model);
return reply.code(200).send(transmissions);
} catch (error) {
logger.error('Error getting dropdown transmissions', { error, year: request.query?.year, make: request.query?.make, model: request.query?.model });
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get transmissions'
});
}
}
Changes:
- Line 195: Querystring types changed to strings
- Line 197: Destructure string parameters
- Line 198: Validation uses
.trim().length === 0 - Line 201: Error message updated
- Line 205: Service call uses strings (now fetches real data!)
- Line 208: Logger uses strings
Testing & Verification
Manual API Testing
Test each endpoint with the new string-based parameters:
# Assuming backend is running on localhost:3000
# Test getMakes
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=2024" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["Acura", "Audi", "BMW", "Ford", "Honda", ...]
# Test getModels (note: make parameter, not make_id)
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=Ford" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["Bronco", "Edge", "Escape", "Explorer", "F-150", "Mustang", ...]
# Test getTrims
curl -X GET "http://localhost:3000/api/vehicles/dropdown/trims?year=2024&make=Ford&model=F-150" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["King Ranch", "Lariat", "Limited", "Platinum", "XL", "XLT", ...]
# Test getEngines
curl -X GET "http://localhost:3000/api/vehicles/dropdown/engines?year=2024&make=Ford&model=F-150&trim=XLT" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["V6 2.7L Turbo", "V6 3.5L Turbo", "V8 5.0L", ...]
# Test getTransmissions (should return REAL data now, not hardcoded)
curl -X GET "http://localhost:3000/api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["10-Speed Automatic", ...] NOT ["Automatic", "Manual"]
# Test with Tesla (electric vehicle - should show N/A for engine)
curl -X GET "http://localhost:3000/api/vehicles/dropdown/engines?year=2024&make=Tesla&model=Model 3&trim=Long Range" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: ["N/A (Electric)"]
Test Error Handling
# Test missing make parameter
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: 400 Bad Request
# Test empty make parameter
curl -X GET "http://localhost:3000/api/vehicles/dropdown/models?year=2024&make=" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: 400 Bad Request
# Test invalid year
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=1900" \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: 400 Bad Request
Integration Tests
Create automated tests:
// backend/src/features/vehicles/tests/integration/dropdown-api.test.ts
describe('Dropdown API Endpoints', () => {
let app: FastifyInstance;
let authToken: string;
beforeAll(async () => {
app = await createTestApp();
authToken = await getTestAuthToken();
});
describe('GET /api/vehicles/dropdown/makes', () => {
it('should return string array of makes', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/vehicles/dropdown/makes?year=2024',
headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.statusCode).toBe(200);
const makes = JSON.parse(response.body);
expect(Array.isArray(makes)).toBe(true);
expect(typeof makes[0]).toBe('string');
expect(makes).toContain('Ford');
});
});
describe('GET /api/vehicles/dropdown/models', () => {
it('should accept make string parameter', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/vehicles/dropdown/models?year=2024&make=Ford',
headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.statusCode).toBe(200);
const models = JSON.parse(response.body);
expect(Array.isArray(models)).toBe(true);
expect(typeof models[0]).toBe('string');
expect(models).toContain('F-150');
});
it('should return 400 for missing make', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/vehicles/dropdown/models?year=2024',
headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.statusCode).toBe(400);
});
});
describe('GET /api/vehicles/dropdown/transmissions', () => {
it('should return real transmission data (not hardcoded)', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150',
headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.statusCode).toBe(200);
const transmissions = JSON.parse(response.body);
expect(Array.isArray(transmissions)).toBe(true);
expect(transmissions.length).toBeGreaterThan(0);
// Should NOT be the old hardcoded values
const hasDetailedTransmissions = transmissions.some((t: string) =>
t.includes('Speed') || t.includes('CVT')
);
expect(hasDetailedTransmissions).toBe(true);
});
});
});
Completion Checklist
Before signaling completion:
- Service methods updated (all use string parameters)
- Service return types changed to string[]
- getDropdownTransmissions() now fetches real data (not hardcoded)
- Controller query parameter types updated (make, model, trim not IDs)
- Controller validation updated (string length checks)
- Controller error messages updated
- All manual API tests pass
- Integration tests pass
- TypeScript compiles with no errors
- Backend builds successfully:
npm run build - No console errors in logs
Common Issues
Issue: "Cannot read property 'trim' of undefined"
Cause: Query parameters might be undefined if not provided
Solution:
// Add extra validation before .trim()
if (!make || typeof make !== 'string' || make.trim().length === 0) {
return reply.code(400).send({ ... });
}
Issue: Transmissions still returning ["Automatic", "Manual"]
Cause: Service method not updated or cache not cleared
Solution:
# Clear Redis cache
docker exec mvp-redis redis-cli FLUSHDB
# Restart backend
make rebuild
Issue: Frontend still sending make_id parameter
Cause: Frontend not yet updated (Agent 5 not complete)
Solution:
- This is expected - Agent 5 will update frontend API client
- For now, test with curl using string parameters
- Document breaking changes for Agent 5
API Contract for Agent 5
Once complete, provide this to Agent 5 (Frontend API Client):
New API Endpoints
All endpoints now use string parameters (not IDs):
// Makes
GET /api/vehicles/dropdown/makes?year=2024
Response: string[] // ["Ford", "Honda", ...]
// Models
GET /api/vehicles/dropdown/models?year=2024&make=Ford
Response: string[] // ["F-150", "Mustang", ...]
// Trims
GET /api/vehicles/dropdown/trims?year=2024&make=Ford&model=F-150
Response: string[] // ["XLT", "Lariat", ...]
// Engines
GET /api/vehicles/dropdown/engines?year=2024&make=Ford&model=F-150&trim=XLT
Response: string[] // ["V8 5.0L", ...]
// Transmissions
GET /api/vehicles/dropdown/transmissions?year=2024&make=Ford&model=F-150
Response: string[] // ["10-Speed Automatic", ...]
// Years (unchanged)
GET /api/vehicles/dropdown/years
Response: number[] // [2026, 2025, ...]
Breaking Changes
Query Parameters:
make_id→make(string)model_id→model(string)trim_id→trim(string)
Response Format:
- Old:
[{id: 1, name: "Ford"}, ...] - New:
["Ford", ...]
Completion Message Template
Agent 4 (Vehicles API): COMPLETE
Files Modified:
- backend/src/features/vehicles/api/vehicles.controller.ts
- backend/src/features/vehicles/domain/vehicles.service.ts
Changes Made:
- Updated all controller query parameters to use strings (make, model, trim)
- Updated all service methods to accept string parameters
- Changed return types to string[] (removed objects)
- getDropdownTransmissions() now fetches real data (not hardcoded!)
- Updated validation to check string lengths
- Updated error messages to reflect new parameter names
Verification:
✓ TypeScript compiles successfully
✓ Backend builds successfully
✓ Manual API tests with curl pass
✓ Integration tests pass
✓ Transmissions return real data (verified)
✓ Electric vehicles show 'N/A (Electric)' for engines
Agent 5 (Frontend API Client) can now update frontend to use new API contract.
Breaking Changes for Agent 5:
- Query parameters changed: make_id→make, model_id→model, trim_id→trim
- Response format changed: {id, name}[] → string[]
- Cascade queries now use selected string values (not IDs)
Document Version: 1.0 Last Updated: 2025-11-10 Status: Ready for Implementation