Services Documentation
Overview
The services directory (src/services/) contains all API communication logic. It abstracts HTTP requests, handles authentication, manages request/response transformations, and provides type-safe API interactions.
Directory Structure
src/services/
└── api/
├── HR/ # HR-related endpoints (11)
├── audit-trail/ # Audit logging (2)
├── auth.ts # Authentication service
├── beneficiary-enrollment/ # Enrollment workflows (15)
├── beneficiary-portal/ # Portal APIs (2)
├── beneficiary-provider/ # Provider APIs (1)
├── client-company/ # Company management (45)
├── cocolife/ # Cocolife provider (19)
├── etiqa/ # Etiqa provider (17)
├── forgot-password/ # Password recovery (3)
├── generali/ # Generali provider (21)
├── icare/ # Icare provider (6)
├── insurance-company-values/ # Company values (18)
├── insurance-plan-schema.ts # Schema definitions
├── insurance-provider/ # Provider metadata (1)
├── intellicare/ # Intellicare provider (23)
├── logged-in-users/ # Active sessions (1)
├── maxicare/ # Maxicare provider (17)
├── principal/ # Principal portal (16)
├── reset-password/ # Password reset (3)
├── user/ # User management (2)
├── instance.ts # Axios instances
├── interface.ts # Shared interfaces
└── response.ts # Response typesAPI Architecture
Instance Configuration (instance.ts)
Two Axios instances handle different authentication requirements:
Public API Instance
For unauthenticated endpoints:
export const publicApi = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_ENDPOINT + '/api/v1/public',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});Used for:
- Authentication (login, OTP)
- Password reset
- Public data retrieval
Private API Instance
For authenticated endpoints with dynamic token injection:
export function privateApi(credentials: Credentials) {
return axios.create({
baseURL: process.env.NEXT_PUBLIC_API_ENDPOINT + '/api/v1/private',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${credentials.accessToken}`,
},
});
}Features:
- Bearer token authentication
- Automatic token refresh on 401
- Request/response interceptors
- Audit logging integration
Interceptors
Request interceptor adds audit context:
privateApi.interceptors.request.use((config) => {
// Add audit tracking headers
config.headers['X-User-ID'] = credentials.userId;
config.headers['X-Action'] = config.metadata?.action;
return config;
});Response interceptor handles token refresh:
privateApi.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Attempt token refresh
const newToken = await refreshToken();
// Retry request with new token
}
return Promise.reject(error);
}
);Service Patterns
Standard Service Structure
Each service module follows this pattern:
// services/api/my-feature/index.ts
import { z } from 'zod';
import { privateApi } from '../instance';
// 1. Define schemas
export const MyDataSchema = z.object({
id: z.string().uuid(),
name: z.string(),
status: z.enum(['active', 'inactive']),
});
export type MyData = z.infer<typeof MyDataSchema>;
// 2. Raw API functions
export async function getMyData(credentials: Credentials, params: QueryParams) {
const api = privateApi(credentials);
const response = await api.get('/my-endpoint', { params });
return MyDataSchema.array().parse(response.data);
}
export async function createMyData(credentials: Credentials, data: CreateMyDataInput) {
const api = privateApi(credentials);
const response = await api.post('/my-endpoint', data);
return MyDataSchema.parse(response.data);
}
export async function updateMyData(
credentials: Credentials,
id: string,
data: UpdateMyDataInput
) {
const api = privateApi(credentials);
const response = await api.put(`/my-endpoint/${id}`, data);
return MyDataSchema.parse(response.data);
}
export async function deleteMyData(credentials: Credentials, id: string) {
const api = privateApi(credentials);
await api.delete(`/my-endpoint/${id}`);
}
// 3. React Query hooks (optional, can be in separate hooks file)
export function useMyData(params: QueryParams) {
const { data: session } = useSession();
return useQuery({
queryKey: ['my-data', params],
queryFn: () => getMyData(session!.user, params),
enabled: !!session,
});
}Core Services
Authentication (auth.ts)
Handles all authentication operations.
| Function | Method | Endpoint | Purpose |
|---|---|---|---|
login() | POST | /public/auth/login | Initial login |
requestOTP() | POST | /public/auth/otp | Request OTP code |
verifyOTP() | POST | /public/auth/verify | Verify OTP |
refreshToken() | POST | /private/auth/refresh | Refresh access token |
getSession() | GET | /private/auth/session | Get current session |
logout() | POST | /private/auth/logout | End session |
Example:
import { login, requestOTP, verifyOTP } from '@/services/api/auth';
// Login flow
const { tempToken } = await login({ email, password });
await requestOTP({ tempToken });
const { accessToken, refreshToken } = await verifyOTP({ tempToken, otp });Client Company (client-company/)
Company management operations (45 API functions).
Categories:
- Company CRUD operations
- Company-Provider associations
- Employee management
- Plan assignments
Example:
import {
getClientCompanies,
createClientCompany,
updateClientCompany,
deleteClientCompany,
} from '@/services/api/client-company';
const companies = await getClientCompanies(credentials, { page: 1, limit: 25 });Insurance Providers
Each provider has dedicated API modules:
| Provider | Directory | Functions |
|---|---|---|
| Cocolife | cocolife/ | 19 |
| Etiqa | etiqa/ | 17 |
| Generali | generali/ | 21 |
| Icare | icare/ | 6 |
| Intellicare | intellicare/ | 23 |
| Maxicare | maxicare/ | 17 |
Provider API Pattern:
// services/api/cocolife/index.ts
export async function getCocolifeMembers(credentials: Credentials, params: MemberParams) {
const api = privateApi(credentials);
const response = await api.get('/cocolife/members', { params });
return CocolifeMemberSchema.array().parse(response.data);
}
export async function getCocolifePlans(credentials: Credentials) {
const api = privateApi(credentials);
const response = await api.get('/cocolife/plans');
return CocolifePlanSchema.array().parse(response.data);
}Beneficiary Enrollment (beneficiary-enrollment/)
Enrollment workflow APIs (15 functions).
Operations:
- Submit enrollment
- Get enrollment status
- Update enrollment
- Cancel enrollment
- Upload documents
import {
submitEnrollment,
getEnrollmentStatus,
uploadEnrollmentDocument,
} from '@/services/api/beneficiary-enrollment';HR (HR/)
HR portal operations (11 functions).
Operations:
- Employee management
- Bulk operations
- Reports generation
- Approvals workflow
Audit Trail (audit-trail/)
Audit logging APIs (2 functions).
import { getAuditLogs, getLoggedInUsers } from '@/services/api/audit-trail';
const logs = await getAuditLogs(credentials, { startDate, endDate, action });
const activeUsers = await getLoggedInUsers(credentials);Response Types (response.ts)
Standardized API response types:
// Common response wrapper
export interface ApiResponse<T> {
data: T;
message: string;
status: number;
}
// Paginated response
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// Error response
export interface ApiError {
message: string;
code: string;
errors?: Record<string, string[]>;
}Interfaces (interface.ts)
Shared API interfaces:
export interface Credentials {
accessToken: string;
refreshToken: string;
userId: string;
userType: 'Broker' | 'Principal' | 'Beneficiary';
}
export interface QueryParams {
page?: number;
limit?: number;
sort?: string;
order?: 'asc' | 'desc';
search?: string;
filters?: Record<string, unknown>;
}
export interface ApiConfig {
timeout?: number;
retries?: number;
headers?: Record<string, string>;
}Error Handling
Service-Level Error Handling
import { handleApiError } from '@/utils/error-handler';
export async function getData(credentials: Credentials) {
try {
const api = privateApi(credentials);
const response = await api.get('/data');
return schema.parse(response.data);
} catch (error) {
if (axios.isAxiosError(error)) {
// Handle specific HTTP errors
if (error.response?.status === 404) {
throw new NotFoundError('Data not found');
}
if (error.response?.status === 403) {
throw new ForbiddenError('Access denied');
}
}
throw error;
}
}Error Handler Utility
// utils/error-handler.ts
export function handleApiError(error: unknown): ApiError {
if (axios.isAxiosError(error)) {
return {
message: error.response?.data?.message || error.message,
code: error.code || 'UNKNOWN_ERROR',
status: error.response?.status,
};
}
return {
message: 'An unexpected error occurred',
code: 'UNKNOWN_ERROR',
};
}Creating New Services
1. Create Directory
services/api/my-feature/
├── index.ts # Main exports
├── schemas.ts # Zod schemas (optional)
└── types.ts # TypeScript types (optional)2. Define Schema
// services/api/my-feature/schemas.ts
import { z } from 'zod';
export const MyFeatureSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
createdAt: z.string().datetime(),
isActive: z.boolean().default(true),
});
export type MyFeature = z.infer<typeof MyFeatureSchema>;3. Implement API Functions
// services/api/my-feature/index.ts
import { privateApi } from '../instance';
import { MyFeatureSchema } from './schemas';
import type { MyFeature } from './schemas';
export async function getMyFeatures(credentials: Credentials) {
const api = privateApi(credentials);
const response = await api.get('/my-feature');
return MyFeatureSchema.array().parse(response.data);
}
export async function createMyFeature(
credentials: Credentials,
data: Omit<MyFeature, 'id' | 'createdAt'>
) {
const api = privateApi(credentials);
const response = await api.post('/my-feature', data);
return MyFeatureSchema.parse(response.data);
}4. Create React Query Hook
// lib/hooks/my-feature.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getMyFeatures, createMyFeature } from '@/services/api/my-feature';
import { useSession } from 'next-auth/react';
export function useMyFeatures() {
const { data: session } = useSession();
return useQuery({
queryKey: ['my-features'],
queryFn: () => getMyFeatures(session!.user),
enabled: !!session,
});
}
export function useCreateMyFeature() {
const { data: session } = useSession();
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateInput) => createMyFeature(session!.user, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['my-features'] });
},
});
}Best Practices
- Always validate responses with Zod schemas
- Type all inputs and outputs explicitly
- Handle errors gracefully with specific error types
- Use React Query for client-side data fetching
- Invalidate queries after mutations
- Add audit context to requests
- Cache appropriately using React Query cache config
- Log API calls for debugging (dev only)