Frontend Developer Guide: Auto-Generated Types, Services, Hooks, and Integration Flow
Overview
This guide explains how to work with auto-generated types, services, and hooks in the Kulcare frontend ecosystem. It covers the patterns used in queue v2, patient v2, and appointment v2 implementations.
Key Principles
- Use Auto-Generated Types First: Always check
@kulcare/api-client/generated/api-types.tsfor existing types before creating new ones - Domain-Based Organization: All manual services in
@kulcare/shared-servicesare organized by API domain (clinical, identity, billing, etc.) - Extend Rather Than Replace: If auto-generated types don't match frontend needs, extend them using TypeScript utility types
Table of Contents
- Auto-Generated Types
- Service Layer Architecture
- React Query Hooks
- Query Key Factories
- Integration Flow Patterns
- Best Practices
- Usage Examples
Auto-Generated Types
Important: Always check auto-generated types first before creating manual types. See Checking Existing Services, Hooks, and Types.
Source
- Auto-generated from backend OpenAPI specification using
openapi-typescript - Located in:
@kulcare/api-client/src/generated/api-types.ts - Updated automatically when backend API changes
Structure
// Main interfaces in generated/api-types.ts
export interface paths { ... } // Maps URL paths to operations
export interface operations { ... } // Defines all API endpoints
export interface components { ... } // Reusable schemasUsage Pattern
import type { components } from '@kulcare/api-client/generated/api-types';
// Extract specific schemas for convenience
type PatientDetailed = components['schemas']['PatientDetailed'];Type Extension Pattern
Since auto-generated types may not perfectly match frontend requirements, extend them:
// In your domain types file
type GeneratedPatientDetailed = components['schemas']['PatientDetailed'];
export interface PatientDetailed extends Omit<GeneratedPatientDetailed,
'deceased' | 'alive' | 'emergency_contact'
> {
// Override with correct types
deceased?: boolean;
alive?: boolean;
emergency_contact?: PatientEmergencyContact;
}Service Layer Architecture
Location
- Manual services:
@kulcare/shared-services/src/domains/*/api.ts - Purpose: Bridge between auto-generated types and React Query hooks
Structure
// Example from @kulcare/shared-services/src/domains/clinical/api.ts
import { apiClient } from '@kulcare/api-client';
import type { components } from '@kulcare/api-client/generated/api-types';
export async function getPatient(id: string): Promise<PatientDetailed> {
const response = await apiClient.get<{
data: PatientDetailed;
}>(`/clinical/patients/${id}`);
// Unwrap backend response: response.data.data contains the patient
return response.data.data;
}Key Principles
- Use apiClient: Always use the shared
apiClientfor auth, error handling, etc. - Transform Responses: Convert backend response structure to frontend expectations
- Handle Errors: Implement proper error handling and user feedback
- Type Safety: Use auto-generated types where possible, extend when needed
React Query Hooks
Location
@kulcare/shared-services/src/domains/*/hooks.ts
Structure
// Example from @kulcare/shared-services/src/domains/clinical/hooks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getPatient } from './api';
import { clinicalKeys } from './keys';
export function usePatient(id: string) {
return useQuery({
queryKey: clinicalKeys.patients.detail(id),
queryFn: () => getPatient(id),
enabled: !!id,
});
}Mutation Pattern
export function useUpdatePatient() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
data,
}: {
id: string;
data: Parameters<typeof updatePatient>[1];
}) => updatePatient(id, data),
onSuccess: (_, { id }) => {
// Invalidate related queries to refresh data
queryClient.invalidateQueries({
queryKey: clinicalKeys.patients.detail(id),
});
queryClient.invalidateQueries({ queryKey: clinicalKeys.patients.all() });
},
});
}Query Key Factories
Location
@kulcare/shared-services/src/domains/*/keys.ts
Structure
// Example from @kulcare/shared-services/src/domains/clinical/keys.ts
export const clinicalKeys = {
all: ['clinical'] as const,
patients: {
all: () => [...clinicalKeys.all, 'patients'] as const,
list: (params?: Record<string, unknown>) =>
[...clinicalKeys.patients.all(), 'list', params] as const,
detail: (id: string) =>
[...clinicalKeys.patients.all(), 'detail', id] as const,
},
} as const;Benefits
- Consistent query key naming across the application
- Proper cache invalidation hierarchy
- Type safety for query keys
Integration Flow Patterns
1. Component Integration
// Example from PatientsPageV2.tsx
import { usePatients } from "@kulcare/shared-services";
export default function PatientsPageV2() {
const { data, isLoading, isError, error, refetch } = usePatients({
search: debouncedSearch || undefined,
page: filters.page,
per_page: filters.perPage,
});
if (isError) {
return <ErrorComponent error={error} onRetry={refetch} />;
}
return (
<div>
{isLoading && <LoadingSpinner />}
{data?.data.map(patient => (
<PatientRow key={patient.id} patient={patient} />
))}
</div>
);
}2. Data Transformation
Services often transform backend responses to frontend-expected structures:
// In service layer
return {
data,
pagination: {
total: backendPagination?.total_entries || 0,
totalPages: backendPagination?.total_pages || 0,
currentPage: backendPagination?.current_page || 1,
perPage: backendPagination?.per_page || 10,
},
statistics: backendStatistics,
};3. Client-Side Filtering (When Needed)
Sometimes additional client-side filtering is needed alongside server-side:
// In hook layer
const filteredQueue = useMemo(() => {
if (!data?.queue) return [];
return filterItems(data.queue, filters.practitionerId, filters.status);
}, [data?.queue, filters.practitionerId, filters.status]);Best Practices
1. Type Safety (Most Important)
- Always use auto-generated types as the foundation - Check
@kulcare/api-client/generated/api-types.tsfirst - Extend types when the backend schema doesn't match frontend needs
- Use TypeScript utility types like
Omit,Pick, andPartial - Only create manual types when auto-generated types don't exist
2. Error Handling
- Implement proper error boundaries
- Provide user-friendly error messages
- Include retry mechanisms where appropriate
3. Loading States
- Handle loading states consistently
- Use skeleton screens for better UX
- Distinguish between initial loading and refetching
4. Cache Management
- Use proper query key hierarchies
- Implement smart invalidation strategies
- Consider stale-while-revalidate patterns
5. Performance Optimization
- Use
useMemoanduseCallbackappropriately - Implement virtual scrolling for large lists
- Debounce search inputs to prevent excessive API calls
Error Handling Utilities
Overview
The @kulcare/shared-services package provides a centralized error handling utility for consistent API error message extraction across all frontend applications.
Import
import { getAPIErrorMessage } from "@kulcare/shared-services";Functions Available
| Function | Purpose |
|---|---|
getAPIErrorMessage(error, fallbackMessage?) | Extract user-friendly error message |
isNetworkError(error) | Check if error is a network error (no response) |
isAuthError(error) | Check if error is 401 Unauthorized |
isForbiddenError(error) | Check if error is 403 Forbidden |
isValidationError(error) | Check if error is 422 Validation Error |
isRateLimitError(error) | Check if error is 429 Rate Limited |
Basic Usage
import { getAPIErrorMessage } from "@kulcare/shared-services";
import { toast } from "sonner";
try {
await createCategory(data);
toast.success("Created successfully");
} catch (error: unknown) {
console.error("Failed:", error);
toast.error(getAPIErrorMessage(error, "Failed to create category"));
}API Error Format Handled
The utility handles the standard Kulcare API error format:
{
"error": {
"code": "validation_failed",
"message": "Category 'test' already exists",
"details": [
{ "field": "base", "message": "Category 'test' already exists" }
]
}
}Error Message Priority
- API error message (
error.error.message) - First detail message (
error.error.details[0].message) - Network error message (if no response)
- Status-specific messages (401, 403, 404, 429, 500)
- Fallback message provided by caller
- Generic fallback message
Using Type Guards
import {
getAPIErrorMessage,
isNetworkError,
isValidationError
} from "@kulcare/shared-services";
try {
await saveData(payload);
} catch (error: unknown) {
if (isNetworkError(error)) {
toast.error("Please check your internet connection");
} else if (isValidationError(error)) {
toast.error(getAPIErrorMessage(error, "Validation failed"));
} else {
toast.error(getAPIErrorMessage(error, "Something went wrong"));
}
}Best Practices for Error Handling
- Always use
getAPIErrorMessage- Don't extract error messages manually - Provide meaningful fallback messages - Help users understand what failed
- Log errors for debugging - Use
console.errorbefore showing toast - Use type guards for conditional handling - When you need different behavior for different error types
- Type catch blocks as
unknown- TypeScript best practice for error handling
Checking Existing Services, Hooks, and Types
Before creating new services, hooks, or types, always check for existing reusable components:
1. Search for Existing Services
- Check
@kulcare/shared-services/src/domains/*/api.tsfor similar functionality - Look for existing API functions that might meet your needs
- Review existing service functions for similar entities
2. Check Available Hooks
- Examine
@kulcare/shared-services/src/domains/*/hooks.tsfor existing React Query hooks - Look for similar hooks that could be extended or modified
- Verify if existing hooks can be reused with different parameters
3. Review Existing Types
- Check
@kulcare/shared-services/src/domains/*/types.tsfor existing type definitions - Look in
@kulcare/api-client/generated/api-types.tsfor auto-generated types - Consider extending existing types rather than creating new ones
4. Query Key Factories
- Review
@kulcare/shared-services/src/domains/*/keys.tsfor existing query key patterns - Reuse existing key structures when possible
- Follow the same naming conventions as existing keys
5. Search Strategy
# Search for existing services
find frontend/packages/shared-services/src -name "*.ts" -exec grep -l "search_term" {} \;
# Search for existing hooks
find frontend/packages/shared-services/src -name "*hook*.ts" -exec grep -l "relevant_functionality" {} \;
# Search for existing types
find frontend/packages/shared-services/src -name "*types*.ts" -exec grep -l "relevant_type" {} \;Usage Examples
Example 1: Creating a New Domain Service
- Define types in
@kulcare/shared-services/src/domains/new-domain/types.ts:
import type { components } from '@kulcare/api-client/generated/api-types';
// Extend generated types as needed
type GeneratedResource = components['schemas']['SomeResource'];
export interface Resource extends Omit<GeneratedResource, 'field_to_fix'> {
field_to_fix?: string; // Corrected type
}- Create API functions in
@kulcare/shared-services/src/domains/new-domain/api.ts:
import { apiClient } from '@kulcare/api-client';
import type { Resource } from './types';
export async function getResources(params?: {
page?: number;
per_page?: number;
}): Promise<{ data: Resource[]; pagination: PaginationMeta }> {
const response = await apiClient.get<{ data: Resource[] }>(
'/new-domain/resources',
{ params }
);
return {
data: response.data.data,
pagination: {
total: 0, // Backend doesn't return pagination for this endpoint
totalPages: 1,
currentPage: 1,
perPage: params?.per_page || 10,
},
};
}- Create query keys in
@kulcare/shared-services/src/domains/new-domain/keys.ts:
export const newDomainKeys = {
all: ['new-domain'] as const,
resources: {
all: () => [...newDomainKeys.all, 'resources'] as const,
list: (params?: Record<string, unknown>) =>
[...newDomainKeys.resources.all(), 'list', params] as const,
detail: (id: string) =>
[...newDomainKeys.resources.all(), 'detail', id] as const,
},
} as const;- Create React Query hooks in
@kulcare/shared-services/src/domains/new-domain/hooks.ts:
import { useQuery } from '@tanstack/react-query';
import { getResources } from './api';
import { newDomainKeys } from './keys';
export function useResources(params?: Parameters<typeof getResources>[0]) {
return useQuery({
queryKey: newDomainKeys.resources.list(params),
queryFn: () => getResources(params),
});
}Example 2: Using in Components
import { useResources } from "@kulcare/shared-services";
export default function ResourcesPage() {
const { data, isLoading, isError, error, refetch } = useResources({
page: 1,
per_page: 10,
});
if (isError) {
return (
<div>
<p>Error loading resources: {error.message}</p>
<button onClick={() => refetch()}>Retry</button>
</div>
);
}
if (isLoading) {
return <div>Loading resources...</div>;
}
return (
<div>
{data?.data.map(resource => (
<div key={resource.id}>{resource.name}</div>
))}
</div>
);
}Example 3: Creating a Mutation
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createResource } from './api';
import { newDomainKeys } from './keys';
export function useCreateResource() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createResource,
onSuccess: () => {
// Invalidate all resources to refresh the list
queryClient.invalidateQueries({ queryKey: newDomainKeys.resources.all() });
},
});
}Troubleshooting
Common Issues
-
Type Mismatches: If auto-generated types don't match backend responses, extend them rather than creating new types from scratch.
-
Cache Invalidation: Make sure to invalidate the right queries in mutations to keep data fresh.
-
Query Key Consistency: Use the same query keys in both queries and invalidation to ensure proper cache behavior.
-
Async Operations: Remember that React Query hooks are asynchronous; handle loading and error states appropriately.
Authentication and Authorization
Using the New Auth Identity System
The application uses the new @kulcare/auth-identity system that manages facility IDs and other authentication-related information. The new auth system provides access to user context including facility information.
Accessing Facility Information
// Example of accessing facility information in components
import { useProfile } from "@kulcare/auth-identity";
export default function SomeComponent() {
const { currentFacilityId, currentTenantId, selectedProfile } = useProfile();
// Use currentFacilityId in API calls
const { data, isLoading } = useFacilityQueue({
facility_id: currentFacilityId || '',
date: formatDateForApi(filters.date),
});
return (
// Component JSX
);
}Auth Identity Context Structure
The auth identity context provides the following properties:
user: Current authenticated user informationcurrentFacilityId: Active facility ID for the user (from selectedProfile.facilities)currentTenantId: Active tenant ID for the userselectedProfile: Currently selected profile with tenant and facilitiesavailableProfiles: List of all profiles the user has access tohasMultipleProfiles: Boolean indicating if user has multiple profiles
Using Facility ID in API Endpoints
When making API calls that require facility ID, pass it as a parameter:
// Example from QueuePageV2.tsx
export default function QueuePageV2() {
const { currentFacilityId } = useProfile();
const {
queue,
outOfQueue,
statistics,
pagination,
isLoading,
isError,
refetch,
} = useQueueData({
facilityId: currentFacilityId, // Pass facility ID to the hook
filters: apiFilters,
page,
perPage,
});
// Component JSX
}Building Query Parameters with Facility ID
When building query parameters for API calls:
// Example from AppointmentsPageV2.tsx
const queryParams = useMemo(() => {
const params = filters.buildQueryParams();
// Add facility_id if available
if (currentFacilityId) {
params.facility_id = currentFacilityId;
}
return params;
}, [filters, currentFacilityId]);Using Auth in Services
When creating services that require facility context:
// In service functions
export async function getFacilitySpecificData(facilityId: string) {
const response = await apiClient.get(`/clinical/facilities/${facilityId}/some-data`);
return response.data;
}
// Or when facilityId comes from auth context
export async function getUserFacilityData(facilityId: string) {
const response = await apiClient.get(`/clinical/facilities/${facilityId}/some-data`);
return response.data;
}Integration with React Query Hooks
When using auth identity context with React Query hooks:
import { useProfile } from "@kulcare/auth-identity";
import { useFacilityData } from "@kulcare/shared-services";
export default function FacilityAwareComponent() {
const { currentFacilityId } = useProfile();
const { data, isLoading, isError } = useFacilityData({
facilityId: currentFacilityId || '', // Pass facilityId to the hook
// other parameters
});
if (isError) {
return <div>Error loading data</div>;
}
return (
<div>
{isLoading ? <div>Loading...</div> : <div>{/* Render data */}</div>}
</div>
);
}Conclusion
This guide provides the foundation for working with auto-generated types, services, and hooks in the Kulcare frontend ecosystem. Following these patterns ensures consistency, type safety, and optimal performance across the application. The new authentication flow with facility ID integration enables secure access to facility-specific data and proper user context management throughout the application.