Kulcare Docs
General

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

  1. Use Auto-Generated Types First: Always check @kulcare/api-client/generated/api-types.ts for existing types before creating new ones
  2. Domain-Based Organization: All manual services in @kulcare/shared-services are organized by API domain (clinical, identity, billing, etc.)
  3. Extend Rather Than Replace: If auto-generated types don't match frontend needs, extend them using TypeScript utility types

Table of Contents

  1. Auto-Generated Types
  2. Service Layer Architecture
  3. React Query Hooks
  4. Query Key Factories
  5. Integration Flow Patterns
  6. Best Practices
  7. 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 schemas

Usage 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

  1. Use apiClient: Always use the shared apiClient for auth, error handling, etc.
  2. Transform Responses: Convert backend response structure to frontend expectations
  3. Handle Errors: Implement proper error handling and user feedback
  4. 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.ts first
  • Extend types when the backend schema doesn't match frontend needs
  • Use TypeScript utility types like Omit, Pick, and Partial
  • 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 useMemo and useCallback appropriately
  • 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

FunctionPurpose
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

  1. API error message (error.error.message)
  2. First detail message (error.error.details[0].message)
  3. Network error message (if no response)
  4. Status-specific messages (401, 403, 404, 429, 500)
  5. Fallback message provided by caller
  6. 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

  1. Always use getAPIErrorMessage - Don't extract error messages manually
  2. Provide meaningful fallback messages - Help users understand what failed
  3. Log errors for debugging - Use console.error before showing toast
  4. Use type guards for conditional handling - When you need different behavior for different error types
  5. 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.ts for 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.ts for 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.ts for existing type definitions
  • Look in @kulcare/api-client/generated/api-types.ts for auto-generated types
  • Consider extending existing types rather than creating new ones

4. Query Key Factories

  • Review @kulcare/shared-services/src/domains/*/keys.ts for 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

  1. 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
}
  1. 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,
    },
  };
}
  1. 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;
  1. 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

  1. Type Mismatches: If auto-generated types don't match backend responses, extend them rather than creating new types from scratch.

  2. Cache Invalidation: Make sure to invalidate the right queries in mutations to keep data fresh.

  3. Query Key Consistency: Use the same query keys in both queries and invalidation to ensure proper cache behavior.

  4. 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 information
  • currentFacilityId: Active facility ID for the user (from selectedProfile.facilities)
  • currentTenantId: Active tenant ID for the user
  • selectedProfile: Currently selected profile with tenant and facilities
  • availableProfiles: List of all profiles the user has access to
  • hasMultipleProfiles: 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.

On this page