Creating a New Feature
Follow this end-to-end tutorial to scaffold and implement a new domain feature adhering to BonardaHR project patterns.
6-Step Implementation Recipe​
Let's walk through creating an example feature: benefits.
Step 1: Define TypeScript Types & Models​
Create src/features/benefits/types/benefits.types.ts:
export interface BenefitPlan {
id: string;
name: string;
provider: string;
coverageType: 'HEALTH' | 'DENTAL' | 'VISION' | 'RETIREMENT';
monthlyCost: number;
isActive: boolean;
}
export interface CreateBenefitPlanDTO {
name: string;
provider: string;
coverageType: string;
monthlyCost: number;
}
Step 2: Implement Axios API Service​
Create src/features/benefits/services/benefitService.ts:
import apiClient from '../../../api/apiClient';
import { BenefitPlan, CreateBenefitPlanDTO } from '../types/benefits.types';
export const benefitService = {
getAll: async (): Promise<BenefitPlan[]> => {
const response = await apiClient.get<BenefitPlan[]>('/benefits');
return response.data;
},
create: async (data: CreateBenefitPlanDTO): Promise<BenefitPlan> => {
const response = await apiClient.post<BenefitPlan>('/benefits', data);
return response.data;
},
};
Step 3: Create React Query Hooks​
Create src/features/benefits/hooks/useBenefits.ts:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { benefitService } from '../services/benefitService';
import { CreateBenefitPlanDTO } from '../types/benefits.types';
export const benefitKeys = {
all: ['benefits'] as const,
lists: () => [...benefitKeys.all, 'list'] as const,
};
export function useBenefits() {
return useQuery({
queryKey: benefitKeys.lists(),
queryFn: benefitService.getAll,
});
}
export function useCreateBenefit() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateBenefitPlanDTO) => benefitService.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: benefitKeys.lists() });
},
});
}
Step 4: Build UI Components​
Create src/features/benefits/components/BenefitsPage.tsx:
import PageHeader from '../../../shared/components/ui/PageHeader';
import LoadingSpinner from '../../../shared/components/ui/LoadingSpinner';
import EmptyState from '../../../shared/components/ui/EmptyState';
import { useBenefits } from '../hooks/useBenefits';
export default function BenefitsPage() {
const { data: benefits, isLoading } = useBenefits();
if (isLoading) return <LoadingSpinner />;
return (
<div className="p-6 space-y-6">
<PageHeader
title="Benefits & Perks"
subtitle="Manage employee insurance and retirement benefit plans"
/>
{!benefits?.length ? (
<EmptyState
title="No benefit plans found"
description="Create your first company benefit plan to get started."
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{benefits.map((plan) => (
<div key={plan.id} className="p-4 border rounded-lg bg-white shadow-sm">
<h3 className="font-semibold text-lg">{plan.name}</h3>
<p className="text-sm text-gray-500">{plan.provider} — ${plan.monthlyCost}/mo</p>
</div>
))}
</div>
)}
</div>
);
}
Step 5: Register Route in AppRoutes.tsx​
Add your new route inside the authenticated layout children in src/routes/AppRoutes.tsx:
import BenefitsPage from '../features/benefits/components/BenefitsPage';
// Inside ProtectedRoute children:
{
path: '/benefits',
element: (
<PermissionGuard permission="BENEFIT_READ">
<BenefitsPage />
</PermissionGuard>
),
}
Step 6: Add Link to Navigation Sidebar​
Add the menu item in src/shared/components/layout/Sidebar.tsx with role/permission gating.