Building a FHIR Frontend with Next.js
By David Le -- Part 11 of the FhirHub Series
FhirHub's frontend is a Next.js 16 application with 60+ React components organized across 12 categories. It uses the App Router with route groups to separate authentication flows from the dashboard, DaisyUI for theming, and a layered architecture that keeps FHIR complexity out of the UI.
App Router Structure
The frontend uses Next.js App Router with two route groups:
frontend/src/app/
├── (auth)/ # Public authentication routes
│ ├── login/page.tsx
│ └── register/page.tsx
├── (dashboard)/ # Protected dashboard routes
│ ├── dashboard/page.tsx
│ ├── patients/
│ │ ├── page.tsx # Patient list
│ │ ├── new/page.tsx # Create patient
│ │ └── [id]/page.tsx # Patient detail
│ ├── export/page.tsx # Bulk data export
│ ├── alerts/page.tsx # Clinical alerts
│ ├── activity/page.tsx # Activity feed
│ ├── smart-launch/page.tsx # SMART on FHIR tools
│ └── admin/
│ ├── users/
│ │ ├── page.tsx # User management
│ │ └── [id]/page.tsx # User detail
│ └── audit/page.tsx # Audit logs
└── page.tsx # Root redirect
Route groups (parentheses in folder names) let us apply different layouts without affecting the URL:
(auth)uses a minimal layout (no sidebar, no header)(dashboard)uses the full application layout with sidebar navigation, header, and role-based menu items
Component Library
The 60+ components are organized into 12 categories:
| Category | Count | Examples |
|---|---|---|
ui/ | 8 | Button, Badge, Modal, Toast, LoadingSkeleton, Icons |
patients/ | 12 | PatientTable, PatientGrid, VitalsChart, LabsPanel, TimelineView |
dashboard/ | 5 | MetricsRow, ActivityFeed, AlertsPanel, RecentPatientsList, SystemStatus |
forms/ | 5 | FormField, DatePicker, FilterPills, SelectDropdown, SearchInput |
export/ | 2 | ExportWizard, ExportJobList |
smart/ | 3 | LaunchSimulator, TokenInspector, ScopeVisualizer |
layout/ | 3 | AppLayout, Header, Sidebar |
common/ | 4 | DataTable, PatientCard, StatsCard, PatientSelector |
fhir/ | 3 | IdentifierDisplay, CodingDisplay, FhirDate |
errors/ | 3 | ErrorBoundary, ApiErrorDisplay, ResourceNotFound |
auth/ | 2 | RoleGate, RouteGuard |
admin/ | 5 | UserTable, UserRoleEditor, InviteUserModal, UserSessionsPanel, AuditLogTable |
FHIR-Specific Components
The fhir/ category contains components that understand FHIR data structures:
- IdentifierDisplay -- Renders FHIR Identifier types with system and value
- CodingDisplay -- Renders CodeableConcept with system, code, and display
- FhirDate -- Handles FHIR date, dateTime, and instant formats
These encapsulate FHIR display logic so patient-facing components don't need to parse FHIR structures.
Auth Components
- RouteGuard -- Wraps protected routes, redirects unauthenticated users to login
- RoleGate -- Conditionally renders content based on the user's Keycloak roles
<RoleGate roles={["admin", "practitioner"]}>
<AddConditionButton />
</RoleGate>
Layered Architecture
The frontend follows a clean layered architecture:
Components (UI layer)
↓
Hooks (state management)
↓
Services (business logic)
↓
Repositories (API client)
↓
.NET API Gateway
Repositories
Repositories handle HTTP communication with the API:
frontend/src/repositories/api/
├── export.repository.ts
└── ...
Each repository maps to an API controller and returns typed DTOs.
Services
Services contain business logic that's independent of UI:
frontend/src/services/
├── export.service.ts
└── ...
Services orchestrate repository calls, handle caching, and transform data.
Custom Hooks
Hooks connect services to React components, managing loading states, errors, and data refresh:
frontend/src/hooks/
├── use-patients.ts
├── use-vitals.ts
└── ...
Styling with Tailwind CSS 4 + DaisyUI 5
FhirHub uses Tailwind CSS 4 with DaisyUI 5 for a consistent, accessible design system:
- DaisyUI provides pre-built components (buttons, badges, modals, tabs) with semantic class names
- Tailwind handles custom spacing, responsive breakpoints, and utility classes
- tailwind-merge resolves class conflicts when components accept custom classNames:
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: (string | undefined | false)[]) {
return twMerge(clsx(inputs));
}
DaisyUI's theming system means the entire app can switch themes (light, dark, etc.) with a single class change.
Validation with Zod
Frontend form validation uses Zod for runtime type checking:
// Zod schemas validate data at runtime
import { z } from "zod";
const PatientSchema = z.object({
name: z.string().min(1, "Name is required"),
birthDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid date format"),
gender: z.enum(["male", "female", "other", "unknown"]),
});
Zod is used alongside FluentValidation on the backend -- defense in depth with validation at both layers.
Key Pages
Dashboard (/dashboard)
The main landing page combines multiple data sources:
- Metrics row (patient count, observation count, etc.)
- Recent patients list
- Activity feed (latest clinical events)
- Alerts panel (abnormal values)
- System status (service health)
- Quick actions (common tasks)
Patient Detail (/patients/[id])
The most complex page, with tabbed navigation:
- Overview -- Patient header with demographics
- Vitals -- Interactive chart + recording modal
- Conditions -- List with add condition modal
- Medications -- List with add medication modal
- Labs -- Panels with reference range highlighting
- Timeline -- Chronological event view
Export (/export)
Two-part page:
- Export wizard (create new export)
- Export job list (monitor existing exports)
SMART Launch (/smart-launch)
Developer tools for SMART on FHIR:
- Launch simulator
- Token inspector
- Scope visualizer
What's Next
In Part 12, we'll examine Keycloak configuration -- realm setup, client configuration with PKCE, the role hierarchy, and claims transformation.