## Technical Design: API Resource Standard ### Overview This design establishes Laravel API Resources as the standard for all API responses, ensuring consistent `"data"` wrapper across all endpoints. --- ### Directory Structure ``` backend/app/Http/Resources/ ├── BaseResource.php # Abstract base with common utilities ├── UserResource.php ├── RoleResource.php ├── TeamMemberResource.php ├── ProjectStatusResource.php ├── ProjectTypeResource.php ├── ProjectResource.php ├── HolidayResource.php ├── PtoResource.php ├── CapacityResource.php ├── TeamCapacityResource.php └── RevenueResource.php ``` --- ### Base Resource Design ```php toIso8601String(); } /** * Format decimal values consistently */ protected function formatDecimal($value, int $decimals = 2): ?float { return $value !== null ? round((float) $value, $decimals) : null; } } ``` --- ### Resource Implementation Pattern Each resource follows this pattern: ```php $this->id, 'name' => $this->name, 'role' => new RoleResource($this->whenLoaded('role')), 'hourly_rate' => $this->formatDecimal($this->hourly_rate), 'active' => (bool) $this->active, 'created_at' => $this->formatDate($this->created_at), 'updated_at' => $this->formatDate($this->updated_at), ]; } } ``` --- ### Controller Update Pattern **BEFORE:** ```php public function index(Request $request): JsonResponse { $members = $this->teamMemberService->getAll(); return response()->json($members); } ``` **AFTER:** ```php public function index(Request $request): JsonResponse { $members = $this->teamMemberService->getAll(); return response()->json(new TeamMemberResource($members)); // Or for collections: // return response()->json(TeamMemberResource::collection($members)); } ``` --- ### Relationship Loading Strategy Use Laravel's `whenLoaded()` to conditionally include relationships: ```php public function toArray($request): array { return [ 'id' => $this->id, 'name' => $this->name, // Only include role if it was eager loaded 'role' => new RoleResource($this->whenLoaded('role')), // Only include team_member if it was eager loaded 'team_member' => new TeamMemberResource($this->whenLoaded('teamMember')), ]; } ``` **Controller Responsibility:** ```php // Eager load relationships before passing to resource $members = TeamMember::with('role')->get(); return response()->json(TeamMemberResource::collection($members)); ``` --- ### Frontend API Client Update **Create helper function to unwrap responses:** ```typescript // src/lib/api/client.ts export async function unwrapResponse(response: Response): Promise { const data = await response.json(); return data.data as T; } // Usage in API functions: export async function getTeamMembers(): Promise { const response = await fetch('/api/team-members'); return unwrapResponse(response); } ``` **Update all API calls:** ```typescript // BEFORE: const members = await response.json(); // AFTER: const members = await unwrapResponse(response); ``` --- ### Test Update Pattern **Backend Feature Tests:** ```php // BEFORE: $response->assertJson(['name' => 'John Doe']); // AFTER: $response->assertJson(['data' => ['name' => 'John Doe']]); // Or more explicitly: $this->assertEquals('John Doe', $response->json('data.name')); ``` **Resource Unit Tests:** ```php test('resource wraps single model in data key', function () { $member = TeamMember::factory()->create(); $resource = new TeamMemberResource($member); $this->assertArrayHasKey('data', $resource->resolve()); $this->assertEquals($member->id, $resource->resolve()['data']['id']); }); test('collection wraps in data array', function () { $members = TeamMember::factory(3)->create(); $collection = TeamMemberResource::collection($members); $this->assertArrayHasKey('data', $collection->resolve()); $this->assertCount(3, $collection->resolve()['data']); }); ``` --- ### Caching Considerations API Resources don't affect caching strategy - they transform data at response time. Cache storage remains the same: ```php // Cache raw model data (not transformed resources) $cached = Cache::remember('team-members:all', 3600, function () { return TeamMember::with('role')->get(); }); // Transform on response return response()->json(TeamMemberResource::collection($cached)); ``` --- ### Error Response Handling Error responses remain unchanged (no `"data"` wrapper): ```php // Validation errors return response()->json([ 'message' => 'The given data was invalid.', 'errors' => $validator->errors(), ], 422); // Not found return response()->json([ 'message' => 'Resource not found', ], 404); ``` --- ### Scribe Documentation Updates Update all `@response` annotations to show new format: ```php /** * @response { * "data": { * "id": "550e8400-e29b-41d4-a716-446655440000", * "name": "John Doe" * } * } */ ``` --- ### Migration Steps 1. **Phase 1: Resources** - Create all resource classes 2. **Phase 2: Controllers** - Update one controller at a time, run tests 3. **Phase 3: Frontend** - Update API client helper, then each endpoint 4. **Phase 4: Tests** - Update all test assertions 5. **Phase 5: Docs** - Regenerate Scribe documentation --- ### Rollback Plan Since this is a breaking change for frontend: - Commit after each controller update - Run full test suite before next controller - If critical issue found, revert specific controller commit