Files
motovaultpro/docs/changes/database-20251111/frontend-api-client.md
2025-11-11 10:29:02 -06:00

18 KiB

Frontend API Client Update - Agent 5

Task: Update vehicles API client to use string-based parameters and responses

Status: Ready for Implementation Dependencies: Agent 4 (Vehicles API) must be complete Estimated Time: 1 hour Assigned To: Agent 5 (Frontend API)


Overview

Update the frontend API client to match the new backend API contract. Change from ID-based parameters to string-based parameters, and handle string array responses instead of object arrays.


Prerequisites

Verify Agent 4 Completed

# Test backend API returns strings
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=2024" \
  -H "Authorization: Bearer TOKEN"
# Should return: ["Ford", "Honda", ...] not [{id: 1, name: "Ford"}, ...]

Files to Modify

frontend/src/features/vehicles/api/vehicles.api.ts
frontend/src/features/vehicles/types/vehicles.types.ts

Current Implementation Analysis

API Client

File: frontend/src/features/vehicles/api/vehicles.api.ts

Current Methods (ID-based parameters, object responses):

getMakes(year: number): Promise<DropdownOption[]>
getModels(year: number, makeId: number): Promise<DropdownOption[]>
getTrims(year: number, makeId: number, modelId: number): Promise<DropdownOption[]>
getEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<DropdownOption[]>
getTransmissions(year: number, makeId: number, modelId: number): Promise<DropdownOption[]>
getYears(): Promise<number[]>

Type Definition

File: frontend/src/features/vehicles/types/vehicles.types.ts

Current DropdownOption:

export interface DropdownOption {
  id: number;
  name: string;
}

Step 1: Update or Remove DropdownOption Type

File: frontend/src/features/vehicles/types/vehicles.types.ts

Option A: Remove DropdownOption entirely (Recommended)

Find and delete (lines 56-59):

export interface DropdownOption {
  id: number;
  name: string;
}

Reason: API now returns plain string[], no need for wrapper type


Option B: Keep as alias (If other code depends on it)

Replace with:

// Deprecated: Dropdowns now return string[] directly
// Keeping for backward compatibility during migration
export type DropdownOption = string;

Recommendation: Use Option A (delete entirely). Clean break, clear intent.


Step 2: Update API Client Imports

File: frontend/src/features/vehicles/api/vehicles.api.ts

Current (line 6):

import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, DropdownOption, VINDecodeResponse } from '../types/vehicles.types';

New:

import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, VINDecodeResponse } from '../types/vehicles.types';
// DropdownOption removed - using string[] now

Step 3: Update getMakes()

Current (lines 41-44):

getMakes: async (year: number): Promise<DropdownOption[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
  return response.data;
},

New:

getMakes: async (year: number): Promise<string[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
  return response.data;
},

Changes:

  • Line 41: Return type Promise<DropdownOption[]>Promise<string[]>
  • Query parameter unchanged (already uses year)
  • Response data is now string array

Step 4: Update getModels()

Current (lines 46-49):

getModels: async (year: number, makeId: number): Promise<DropdownOption[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make_id=${makeId}`);
  return response.data;
},

New:

getModels: async (year: number, make: string): Promise<string[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make=${encodeURIComponent(make)}`);
  return response.data;
},

Changes:

  • Line 46: Parameter makeId: numbermake: string
  • Line 46: Return type Promise<DropdownOption[]>Promise<string[]>
  • Line 47: Query param make_id=${makeId}make=${encodeURIComponent(make)}
  • Important: Use encodeURIComponent() to handle spaces and special chars in make names

Step 5: Update getTrims()

Current (lines 61-64):

getTrims: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make_id=${makeId}&model_id=${modelId}`);
  return response.data;
},

New:

getTrims: async (year: number, make: string, model: string): Promise<string[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
  return response.data;
},

Changes:

  • Line 61: Parameters changed to make: string, model: string
  • Line 61: Return type changed to Promise<string[]>
  • Line 62: Query params changed to make=...&model=...
  • Use encodeURIComponent() for both make and model

Step 6: Update getEngines()

Current (lines 56-59):

getEngines: async (year: number, makeId: number, modelId: number, trimId: number): Promise<DropdownOption[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make_id=${makeId}&model_id=${modelId}&trim_id=${trimId}`);
  return response.data;
},

New:

getEngines: async (year: number, make: string, model: string, trim: string): Promise<string[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}&trim=${encodeURIComponent(trim)}`);
  return response.data;
},

Changes:

  • Line 56: Parameters changed to strings: make: string, model: string, trim: string
  • Line 56: Return type changed to Promise<string[]>
  • Line 57: Query params changed to use strings with proper encoding
  • Use encodeURIComponent() for all string parameters

Step 7: Update getTransmissions()

Current (lines 51-54):

getTransmissions: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make_id=${makeId}&model_id=${modelId}`);
  return response.data;
},

New:

getTransmissions: async (year: number, make: string, model: string): Promise<string[]> => {
  const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
  return response.data;
},

Changes:

  • Line 51: Parameters changed to make: string, model: string
  • Line 51: Return type changed to Promise<string[]>
  • Line 52: Query params changed to strings
  • Use encodeURIComponent() for encoding

Complete Updated File

After all changes, the vehicles.api.ts file should look like:

/**
 * @ai-summary API calls for vehicles feature
 */

import { apiClient } from '../../../core/api/client';
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, VINDecodeResponse } from '../types/vehicles.types';

// All requests (including dropdowns) use authenticated apiClient

export const vehiclesApi = {
  getAll: async (): Promise<Vehicle[]> => {
    const response = await apiClient.get('/vehicles');
    return response.data;
  },

  getById: async (id: string): Promise<Vehicle> => {
    const response = await apiClient.get(`/vehicles/${id}`);
    return response.data;
  },

  create: async (data: CreateVehicleRequest): Promise<Vehicle> => {
    const response = await apiClient.post('/vehicles', data);
    return response.data;
  },

  update: async (id: string, data: UpdateVehicleRequest): Promise<Vehicle> => {
    const response = await apiClient.put(`/vehicles/${id}`, data);
    return response.data;
  },

  delete: async (id: string): Promise<void> => {
    await apiClient.delete(`/vehicles/${id}`);
  },

  // Dropdown API methods - now return string[] and accept string parameters
  getYears: async (): Promise<number[]> => {
    const response = await apiClient.get('/vehicles/dropdown/years');
    return response.data;
  },

  getMakes: async (year: number): Promise<string[]> => {
    const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
    return response.data;
  },

  getModels: async (year: number, make: string): Promise<string[]> => {
    const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make=${encodeURIComponent(make)}`);
    return response.data;
  },

  getTrims: async (year: number, make: string, model: string): Promise<string[]> => {
    const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
    return response.data;
  },

  getEngines: async (year: number, make: string, model: string, trim: string): Promise<string[]> => {
    const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}&trim=${encodeURIComponent(trim)}`);
    return response.data;
  },

  getTransmissions: async (year: number, make: string, model: string): Promise<string[]> => {
    const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
    return response.data;
  },

  // VIN decode method - using unified platform endpoint
  decodeVIN: async (vin: string): Promise<VINDecodeResponse> => {
    const response = await apiClient.get(`/platform/vehicle?vin=${vin}`);
    return response.data;
  },
};

Why encodeURIComponent()?

Problem: Vehicle makes/models can contain spaces and special characters:

  • "Land Rover"
  • "Mercedes-Benz"
  • "Alfa Romeo"

Without encoding: Query string breaks

/vehicles/dropdown/models?year=2024&make=Land Rover
                                              ^ space breaks URL

With encoding: Query string is valid

/vehicles/dropdown/models?year=2024&make=Land%20Rover

Rule: Always encode user-provided strings in URLs


Testing & Verification

Manual Testing

Test the API client methods in browser console:

// In browser console (after logging in)
import { vehiclesApi } from './src/features/vehicles/api/vehicles.api';

// Test getMakes - should return string[]
const makes = await vehiclesApi.getMakes(2024);
console.log(makes); // Expected: ["Acura", "Audi", "BMW", ...]
console.log(typeof makes[0]); // Expected: "string"

// Test getModels with string parameter
const models = await vehiclesApi.getModels(2024, 'Ford');
console.log(models); // Expected: ["Bronco", "Edge", "F-150", ...]

// Test with space in name
const landRoverModels = await vehiclesApi.getModels(2024, 'Land Rover');
console.log(landRoverModels); // Should work - no URL errors

// Test getTrims
const trims = await vehiclesApi.getTrims(2024, 'Ford', 'F-150');
console.log(trims); // Expected: ["XLT", "Lariat", ...]

// Test getEngines
const engines = await vehiclesApi.getEngines(2024, 'Ford', 'F-150', 'XLT');
console.log(engines); // Expected: ["V6 2.7L Turbo", "V8 5.0L", ...]

// Test getTransmissions - should return REAL data now
const transmissions = await vehiclesApi.getTransmissions(2024, 'Ford', 'F-150');
console.log(transmissions); // Expected: ["10-Speed Automatic", ...] NOT ["Automatic", "Manual"]

Unit Tests

Create tests for the API client:

// frontend/src/features/vehicles/api/__tests__/vehicles.api.test.ts

import { vehiclesApi } from '../vehicles.api';
import { apiClient } from '../../../../core/api/client';

jest.mock('../../../../core/api/client');

describe('vehiclesApi', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('getMakes', () => {
    it('should return string array', async () => {
      const mockMakes = ['Ford', 'Honda', 'Toyota'];
      (apiClient.get as jest.Mock).mockResolvedValue({ data: mockMakes });

      const result = await vehiclesApi.getMakes(2024);

      expect(result).toEqual(mockMakes);
      expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/makes?year=2024');
    });
  });

  describe('getModels', () => {
    it('should use make string parameter', async () => {
      const mockModels = ['F-150', 'Mustang'];
      (apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });

      const result = await vehiclesApi.getModels(2024, 'Ford');

      expect(result).toEqual(mockModels);
      expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/models?year=2024&make=Ford');
    });

    it('should encode make parameter with spaces', async () => {
      const mockModels = ['Range Rover', 'Defender'];
      (apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });

      await vehiclesApi.getModels(2024, 'Land Rover');

      expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/models?year=2024&make=Land%20Rover');
    });

    it('should encode special characters', async () => {
      const mockModels = ['C-Class'];
      (apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });

      await vehiclesApi.getModels(2024, 'Mercedes-Benz');

      expect(apiClient.get).toHaveBeenCalledWith(expect.stringContaining('Mercedes-Benz'));
    });
  });

  describe('getTransmissions', () => {
    it('should return real transmission data', async () => {
      const mockTransmissions = ['10-Speed Automatic', '6-Speed Manual'];
      (apiClient.get as jest.Mock).mockResolvedValue({ data: mockTransmissions });

      const result = await vehiclesApi.getTransmissions(2024, 'Ford', 'F-150');

      expect(result).toEqual(mockTransmissions);
      // Verify not getting old hardcoded data
      expect(result).not.toEqual([{id: 1, name: 'Automatic'}, {id: 2, name: 'Manual'}]);
    });
  });
});

Run tests:

cd frontend
npm test -- vehicles.api.test.ts

Completion Checklist

Before signaling completion:

  • DropdownOption type removed (or deprecated)
  • Import statement updated (removed DropdownOption)
  • getMakes() returns string[]
  • getModels() accepts make string, returns string[]
  • getTrims() accepts make/model strings, returns string[]
  • getEngines() accepts make/model/trim strings, returns string[]
  • getTransmissions() accepts make/model strings, returns string[]
  • All string parameters use encodeURIComponent()
  • Query parameters changed: make_id→make, model_id→model, trim_id→trim
  • Manual tests pass (no type errors, correct responses)
  • Unit tests pass
  • TypeScript compiles with no errors
  • No console errors in browser

Common Issues

Issue: TypeScript error "Type 'string[]' is not assignable to 'DropdownOption[]'"

Cause: Form component (Agent 6) not yet updated, still expects DropdownOption[]

Solution:

  • This is expected - Agent 6 will update the form component
  • Verify API client changes are correct
  • TypeScript errors will resolve once Agent 6 completes

Issue: URL encoding breaks query

Cause: Forgot encodeURIComponent() on string parameters

Solution:

// Wrong
`/models?make=${make}`  // Breaks with "Land Rover"

// Correct
`/models?make=${encodeURIComponent(make)}`  // Handles all cases

Issue: Backend returns 400 "make parameter required"

Cause: Frontend sending wrong parameter name or encoding issue

Solution:

  • Check browser Network tab for actual request
  • Verify parameter names: make, model, trim (not make_id, etc.)
  • Verify values are properly encoded

Handoff to Agent 6

Once complete, provide this information:

Updated API Client Contract

Methods:

getYears(): Promise<number[]>
getMakes(year: number): Promise<string[]>
getModels(year: number, make: string): Promise<string[]>
getTrims(year: number, make: string, model: string): Promise<string[]>
getEngines(year: number, make: string, model: string, trim: string): Promise<string[]>
getTransmissions(year: number, make: string, model: string): Promise<string[]>
decodeVIN(vin: string): Promise<VINDecodeResponse>

Key Changes for Agent 6:

  • All dropdown methods return string[] (not DropdownOption[])
  • Cascade queries use selected string values directly (not IDs)
  • No need to extract .id from selected options anymore
  • No need to find selected option by ID to get name
  • Simpler state management - store strings directly

Example Usage:

// OLD (ID-based)
const [selectedMake, setSelectedMake] = useState<DropdownOption | null>(null);
const models = await vehiclesApi.getModels(year, selectedMake.id);  // Use ID
// Store name: vehicle.make = selectedMake.name

// NEW (String-based)
const [selectedMake, setSelectedMake] = useState<string>('');
const models = await vehiclesApi.getModels(year, selectedMake);  // Use string directly
// Store directly: vehicle.make = selectedMake

Verification Command

# Agent 6 can verify API client is ready:
cd frontend && npm run build
# Should compile (may have type errors in form component - Agent 6 will fix)

Completion Message Template

Agent 5 (Frontend API Client): COMPLETE

Files Modified:
- frontend/src/features/vehicles/api/vehicles.api.ts
- frontend/src/features/vehicles/types/vehicles.types.ts

Changes Made:
- Removed DropdownOption interface (using string[] now)
- Updated all dropdown methods to return string[]
- Changed parameters from IDs to strings (make, model, trim)
- Added encodeURIComponent() for URL encoding
- Query parameters updated: make_id→make, model_id→model, trim_id→trim

Verification:
✓ TypeScript compiles (with expected form component errors)
✓ Manual API tests return correct string arrays
✓ Unit tests pass
✓ URL encoding handles spaces and special characters
✓ Transmissions return real data (not hardcoded)

Agent 6 (Frontend Forms) can now update VehicleForm component to use new API contract.

Breaking Changes for Agent 6:
- API methods return string[] not DropdownOption[]
- No more .id property - use selected strings directly
- Simplified cascade logic - no ID lookups needed
- Form can store selected values directly as strings

Document Version: 1.0 Last Updated: 2025-11-10 Status: Ready for Implementation