# Dependency Injection Plan for GridPilot Website ## Overview Implement proper dependency injection in your website using InversifyJS, following the same patterns as your NestJS API. This replaces the current manual `ServiceFactory` approach with a professional DI container system. ## Why InversifyJS? - **NestJS-like**: Same decorators and patterns you already know - **TypeScript-first**: Excellent type safety - **React-friendly**: Works seamlessly with React Context - **Production-ready**: Battle-tested, well-maintained ## Current Problem ```typescript // Current: Manual dependency management // apps/website/lib/services/ServiceProvider.tsx const services = useMemo(() => { const serviceFactory = new ServiceFactory(getWebsiteApiBaseUrl()); return { leagueService: serviceFactory.createLeagueService(), driverService: serviceFactory.createDriverService(), // ... 25+ services manually created }; }, []); ``` **Issues:** - No formal DI container - Manual dependency wiring - Hard to test/mock - No lifecycle management - Inconsistent with API ## Solution Architecture ### 1. Install Dependencies ```bash npm install inversify reflect-metadata npm install --save-dev @types/inversify ``` ### 2. Configure TypeScript ```json // apps/website/tsconfig.json { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } } ``` ### 3. Core Structure ``` apps/website/lib/di/ ├── index.ts # Main exports ├── container.ts # Container factory ├── tokens.ts # Symbol tokens ├── providers/ │ └── ContainerProvider.tsx ├── hooks/ │ └── useInject.ts └── modules/ ├── core.module.ts ├── api.module.ts ├── league.module.ts └── ... (one per domain) ``` ## Implementation ### Step 1: Token Registry **File**: `apps/website/lib/di/tokens.ts` ```typescript // Centralized token registry export const LOGGER_TOKEN = Symbol.for('Core.Logger'); export const ERROR_REPORTER_TOKEN = Symbol.for('Core.ErrorReporter'); export const LEAGUE_SERVICE_TOKEN = Symbol.for('Service.League'); export const DRIVER_SERVICE_TOKEN = Symbol.for('Service.Driver'); export const TEAM_SERVICE_TOKEN = Symbol.for('Service.Team'); export const RACE_SERVICE_TOKEN = Symbol.for('Service.Race'); // ... all service tokens ``` ### Step 2: Container Factory **File**: `apps/website/lib/di/container.ts` ```typescript import { Container } from 'inversify'; import { CoreModule } from './modules/core.module'; import { ApiModule } from './modules/api.module'; import { LeagueModule } from './modules/league.module'; // ... other modules export function createContainer(): Container { const container = new Container({ defaultScope: 'Singleton' }); container.load( CoreModule, ApiModule, LeagueModule, // ... all modules ); return container; } ``` ### Step 3: React Integration **File**: `apps/website/lib/di/providers/ContainerProvider.tsx` ```typescript 'use client'; import { createContext, ReactNode, useContext, useMemo } from 'react'; import { Container } from 'inversify'; import { createContainer } from '../container'; const ContainerContext = createContext(null); export function ContainerProvider({ children }: { children: ReactNode }) { const container = useMemo(() => createContainer(), []); return ( {children} ); } export function useContainer(): Container { const container = useContext(ContainerContext); if (!container) throw new Error('Missing ContainerProvider'); return container; } ``` ### Step 4: Injection Hook **File**: `apps/website/lib/di/hooks/useInject.ts` ```typescript 'use client'; import { useContext, useMemo } from 'react'; import { ContainerContext } from '../providers/ContainerProvider'; export function useInject(token: symbol): T { const container = useContext(ContainerContext); if (!container) throw new Error('Missing ContainerProvider'); return useMemo(() => container.get(token), [container, token]); } ``` ### Step 5: Module Examples **File**: `apps/website/lib/di/modules/league.module.ts` ```typescript import { ContainerModule } from 'inversify'; import { LeagueService } from '../../services/leagues/LeagueService'; import { LEAGUE_SERVICE_TOKEN, LEAGUE_API_CLIENT_TOKEN, DRIVER_API_CLIENT_TOKEN, SPONSOR_API_CLIENT_TOKEN, RACE_API_CLIENT_TOKEN } from '../tokens'; export const LeagueModule = new ContainerModule((bind) => { bind(LEAGUE_SERVICE_TOKEN) .toDynamicValue((context) => { const leagueApiClient = context.container.get(LEAGUE_API_CLIENT_TOKEN); const driverApiClient = context.container.get(DRIVER_API_CLIENT_TOKEN); const sponsorApiClient = context.container.get(SPONSOR_API_CLIENT_TOKEN); const raceApiClient = context.container.get(RACE_API_CLIENT_TOKEN); return new LeagueService( leagueApiClient, driverApiClient, sponsorApiClient, raceApiClient ); }) .inSingletonScope(); }); ``` **File**: `apps/website/lib/di/modules/api.module.ts` ```typescript import { ContainerModule } from 'inversify'; import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient'; import { DriversApiClient } from '../../api/drivers/DriversApiClient'; // ... other API clients import { LEAGUE_API_CLIENT_TOKEN, DRIVER_API_CLIENT_TOKEN, LOGGER_TOKEN, ERROR_REPORTER_TOKEN, CONFIG_TOKEN } from '../tokens'; export const ApiModule = new ContainerModule((bind) => { const createApiClient = (ClientClass: any, context: any) => { const baseUrl = context.container.get(CONFIG_TOKEN); const errorReporter = context.container.get(ERROR_REPORTER_TOKEN); const logger = context.container.get(LOGGER_TOKEN); return new ClientClass(baseUrl, errorReporter, logger); }; bind(LEAGUE_API_CLIENT_TOKEN) .toDynamicValue(ctx => createApiClient(LeaguesApiClient, ctx)) .inSingletonScope(); bind(DRIVER_API_CLIENT_TOKEN) .toDynamicValue(ctx => createApiClient(DriversApiClient, ctx)) .inSingletonScope(); // ... other API clients }); ``` ## Usage Examples ### In React-Query Hooks (Your Current Pattern) **Before**: ```typescript // apps/website/hooks/useLeagueService.ts export function useAllLeagues() { const { leagueService } = useServices(); // ❌ Manual service retrieval return useQuery({ queryKey: ['allLeagues'], queryFn: () => leagueService.getAllLeagues(), }); } ``` **After**: ```typescript // apps/website/hooks/useLeagueService.ts import { useInject } from '@/lib/di/hooks/useInject'; import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens'; export function useAllLeagues() { const leagueService = useInject(LEAGUE_SERVICE_TOKEN); // ✅ Clean DI return useQuery({ queryKey: ['allLeagues'], queryFn: () => leagueService.getAllLeagues(), }); } ``` ### In React-Query Mutations ```typescript export function useCreateLeague() { const leagueService = useInject(LEAGUE_SERVICE_TOKEN); const queryClient = useQueryClient(); return useMutation({ mutationFn: (input: CreateLeagueInputDTO) => leagueService.createLeague(input), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['allLeagues'] }); }, }); } ``` ### In Components ```typescript 'use client'; import { useInject } from '@/lib/di/hooks/useInject'; import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens'; export function CreateLeagueForm() { const leagueService = useInject(LEAGUE_SERVICE_TOKEN); // Use leagueService directly } ``` ### In Server Components ```typescript import { createContainer } from '@/lib/di/container'; import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens'; export default async function LeaguePage({ params }) { const container = createContainer(); const leagueService = container.get(LEAGUE_SERVICE_TOKEN); const league = await leagueService.getLeague(params.id); return ; } ``` ### In Tests ```typescript import { createTestContainer } from '@/lib/di/container'; import { ContainerProvider } from '@/lib/di/providers/ContainerProvider'; test('useAllLeagues works with DI', () => { const mockLeagueService = { getAllLeagues: jest.fn().mockResolvedValue([{ id: '1', name: 'Test League' }]) }; const overrides = new Map([ [LEAGUE_SERVICE_TOKEN, mockLeagueService] ]); const container = createTestContainer(overrides); const { result } = renderHook(() => useAllLeagues(), { wrapper: ({ children }) => ( {children} ) }); // Test works exactly the same }); ``` ### Complete React-Query Hook Migration Example ```typescript // apps/website/hooks/useLeagueService.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useInject } from '@/lib/di/hooks/useInject'; import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens'; import { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO'; export function useAllLeagues() { const leagueService = useInject(LEAGUE_SERVICE_TOKEN); return useQuery({ queryKey: ['allLeagues'], queryFn: () => leagueService.getAllLeagues(), }); } export function useLeagueStandings(leagueId: string, currentUserId: string) { const leagueService = useInject(LEAGUE_SERVICE_TOKEN); return useQuery({ queryKey: ['leagueStandings', leagueId, currentUserId], queryFn: () => leagueService.getLeagueStandings(leagueId, currentUserId), enabled: !!leagueId && !!currentUserId, }); } export function useCreateLeague() { const leagueService = useInject(LEAGUE_SERVICE_TOKEN); const queryClient = useQueryClient(); return useMutation({ mutationFn: (input: CreateLeagueInputDTO) => leagueService.createLeague(input), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['allLeagues'] }); }, }); } export function useRemoveLeagueMember() { const leagueService = useInject(LEAGUE_SERVICE_TOKEN); const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ leagueId, performerDriverId, targetDriverId }: { leagueId: string; performerDriverId: string; targetDriverId: string; }) => leagueService.removeMember(leagueId, performerDriverId, targetDriverId), onSuccess: (data, variables) => { queryClient.invalidateQueries({ queryKey: ['leagueMemberships', variables.leagueId] }); queryClient.invalidateQueries({ queryKey: ['leagueStandings', variables.leagueId] }); }, }); } ``` ### Multiple Services in One Hook ```typescript import { useInjectMany } from '@/lib/di/hooks/useInjectMany'; import { LEAGUE_SERVICE_TOKEN, RACE_SERVICE_TOKEN } from '@/lib/di/tokens'; export function useLeagueAndRaceData(leagueId: string) { const [leagueService, raceService] = useInjectMany([ LEAGUE_SERVICE_TOKEN, RACE_SERVICE_TOKEN ]); const leagueQuery = useQuery({ queryKey: ['league', leagueId], queryFn: () => leagueService.getLeague(leagueId), }); const racesQuery = useQuery({ queryKey: ['races', leagueId], queryFn: () => raceService.getRacesByLeague(leagueId), }); return { leagueQuery, racesQuery }; } ``` ## Migration Strategy ### Phase 1: Setup (Week 1) 1. Install dependencies 2. Configure TypeScript 3. Create core infrastructure 4. Create API module ### Phase 2: Domain Modules (Week 2-3) 1. Create module for each domain (2-3 per day) 2. Register all services 3. Test each module ### Phase 3: React-Query Hooks Migration (Week 4) 1. Update all hooks to use `useInject()` instead of `useServices()` 2. Migrate one domain at a time: - `useLeagueService.ts` → `useInject(LEAGUE_SERVICE_TOKEN)` - `useRaceService.ts` → `useInject(RACE_SERVICE_TOKEN)` - `useDriverService.ts` → `useInject(DRIVER_SERVICE_TOKEN)` - etc. 3. Test each hook after migration ### Phase 4: Integration & Cleanup (Week 5) 1. Update root layout with ContainerProvider 2. Remove old ServiceProvider 3. Remove ServiceFactory 4. Final verification ### React-Query Hook Migration Pattern Each hook file gets a simple 2-line change: ```typescript // Before import { useServices } from '@/lib/services/ServiceProvider'; const { leagueService } = useServices(); // After import { useInject } from '@/lib/di/hooks/useInject'; import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens'; const leagueService = useInject(LEAGUE_SERVICE_TOKEN); ``` The rest of the hook (queries, mutations, etc.) stays exactly the same. ## Benefits ✅ **Testability**: Easy mocking via container overrides ✅ **Maintainability**: Clear dependency graphs ✅ **Type Safety**: Compile-time validation ✅ **Consistency**: Same patterns as API ✅ **Performance**: Singleton scope by default ## Files to Create 1. `apps/website/lib/di/index.ts` 2. `apps/website/lib/di/container.ts` 3. `apps/website/lib/di/tokens.ts` 4. `apps/website/lib/di/providers/ContainerProvider.tsx` 5. `apps/website/lib/di/hooks/useInject.ts` 6. `apps/website/lib/di/modules/core.module.ts` 7. `apps/website/lib/di/modules/api.module.ts` 8. `apps/website/lib/di/modules/league.module.ts` 9. `apps/website/lib/di/modules/driver.module.ts` 10. `apps/website/lib/di/modules/team.module.ts` 11. `apps/website/lib/di/modules/race.module.ts` 12. `apps/website/lib/di/modules/media.module.ts` 13. `apps/website/lib/di/modules/payment.module.ts` 14. `apps/website/lib/di/modules/analytics.module.ts` 15. `apps/website/lib/di/modules/auth.module.ts` 16. `apps/website/lib/di/modules/dashboard.module.ts` 17. `apps/website/lib/di/modules/policy.module.ts` 18. `apps/website/lib/di/modules/protest.module.ts` 19. `apps/website/lib/di/modules/penalty.module.ts` 20. `apps/website/lib/di/modules/onboarding.module.ts` 21. `apps/website/lib/di/modules/landing.module.ts` ## Next Steps 1. **Approve this plan** 2. **Start Phase 1**: Install dependencies and create core infrastructure 3. **Proceed module by module**: No backward compatibility needed - clean migration **Total effort**: 4-5 weeks for clean implementation