Data Patterns
Server Action Pattern
Basic Pattern
// app/actions/patient-actions.ts
'use server'
export async function createPatient(data: PatientInput) {
// Validate input
const validated = patientSchema.parse(data)
// Perform mutation
const patient = await db.patients.create(validated)
// Revalidate cache
revalidatePath('/patients')
return { success: true, patient }
}
Error Handling
export async function updatePatient(id: string, data: PatientInput) {
'use server'
try {
const validated = patientSchema.parse(data)
const patient = await db.patients.update(id, validated)
if (!patient) {
return { error: 'Patient not found' }
}
revalidatePath(`/patients/${id}`)
return { success: true, patient }
} catch (error) {
if (error instanceof z.ZodError) {
return { error: 'Invalid data', details: error.errors }
}
return { error: 'Failed to update patient' }
}
}
FHIR Data Transformation Pattern
Parsing Pattern
export function parseFHIRPatient(fhirPatient: fhir4.Patient): PatientData {
return {
id: fhirPatient.id || generateId(),
name: extractName(fhirPatient.name),
mrn: extractMRN(fhirPatient.identifier),
birthDate: fhirPatient.birthDate,
gender: fhirPatient.gender,
contact: extractContact(fhirPatient.telecom),
address: extractAddress(fhirPatient.address),
}
}
function extractName(names?: fhir4.HumanName[]): string {
if (!names?.length) return 'Unknown'
const name = names[0]
return [name.given?.join(' '), name.family]
.filter(Boolean)
.join(' ')
}
Bundle Processing
export function processFHIRBundle(bundle: fhir4.Bundle): ProcessedData {
const resources = bundle.entry?.map(e => e.resource) || []
return {
patients: resources
.filter(r => r?.resourceType === 'Patient')
.map(r => parseFHIRPatient(r as fhir4.Patient)),
conditions: resources
.filter(r => r?.resourceType === 'Condition')
.map(r => parseCondition(r as fhir4.Condition)),
medications: resources
.filter(r => r?.resourceType === 'MedicationRequest')
.map(r => parseMedication(r as fhir4.MedicationRequest)),
}
}
Repository Pattern
Interface Definition
interface PatientRepository {
findAll(): Promise<Patient[]>
findById(id: string): Promise<Patient | null>
create(data: CreatePatientInput): Promise<Patient>
update(id: string, data: UpdatePatientInput): Promise<Patient>
delete(id: string): Promise<void>
}
Mock Implementation
class MockPatientRepository implements PatientRepository {
private patients: Map<string, Patient> = new Map()
async findAll(): Promise<Patient[]> {
return Array.from(this.patients.values())
}
async findById(id: string): Promise<Patient | null> {
return this.patients.get(id) || null
}
async create(data: CreatePatientInput): Promise<Patient> {
const patient = {
id: generateId(),
...data,
createdAt: new Date(),
}
this.patients.set(patient.id, patient)
return patient
}
}
Database Implementation
class SupabasePatientRepository implements PatientRepository {
constructor(private supabase: SupabaseClient) {}
async findAll(): Promise<Patient[]> {
const { data, error } = await this.supabase
.from('patients')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return data
}
async create(data: CreatePatientInput): Promise<Patient> {
const { data: patient, error } = await this.supabase
.from('patients')
.insert(data)
.select()
.single()
if (error) throw error
return patient
}
}
Data Validation Pattern
Zod Schema Pattern
export const patientSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email().optional(),
phone: z.string().regex(/^\+?[\d\s-()]+$/),
dateOfBirth: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
gender: z.enum(['male', 'female', 'other']),
address: addressSchema.optional(),
})
export const addressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
})
Validation Usage
export async function validatePatientData(data: unknown) {
try {
return {
success: true,
data: patientSchema.parse(data),
}
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
errors: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
})),
}
}
throw error
}
}
Caching Pattern
Revalidation Strategy
// Revalidate specific paths
export async function updatePatientAndRevalidate(
id: string,
data: PatientInput
) {
const patient = await updatePatient(id, data)
// Revalidate multiple related paths
revalidatePath(`/patients/${id}`)
revalidatePath('/patients')
revalidatePath('/dashboard')
return patient
}
Cache Tags Pattern
// Tag-based revalidation (future Next.js feature)
export async function getPatient(id: string) {
const patient = await db.patients.findById(id)
// Tag this data for revalidation
unstable_cache(
async () => patient,
[`patient-${id}`],
{
tags: [`patient-${id}`, 'patients'],
}
)
return patient
}
Optimistic Update Pattern
Client Component
'use client'
export function PatientCard({ patient }: Props) {
const [optimisticPatient, setOptimisticPatient] = useState(patient)
async function handleUpdate(data: PatientInput) {
// Optimistic update
setOptimisticPatient({ ...optimisticPatient, ...data })
// Server update
const result = await updatePatient(patient.id, data)
if (!result.success) {
// Revert on error
setOptimisticPatient(patient)
toast.error('Failed to update')
}
}
return <Card patient={optimisticPatient} onUpdate={handleUpdate} />
}
Pagination Pattern
Server-Side Pagination
export async function getPatients(
page: number = 1,
limit: number = 10
) {
const offset = (page - 1) * limit
const { data, count } = await supabase
.from('patients')
.select('*', { count: 'exact' })
.range(offset, offset + limit - 1)
.order('created_at', { ascending: false })
return {
patients: data,
pagination: {
page,
limit,
total: count,
totalPages: Math.ceil(count / limit),
},
}
}
Cursor-Based Pagination
export async function getPatientsCursor(
cursor?: string,
limit: number = 10
) {
let query = supabase
.from('patients')
.select('*')
.order('created_at', { ascending: false })
.limit(limit + 1) // Fetch extra to check hasMore
if (cursor) {
query = query.lt('created_at', cursor)
}
const { data } = await query
const hasMore = data.length > limit
const patients = hasMore ? data.slice(0, -1) : data
const nextCursor = hasMore ? patients[patients.length - 1].created_at : null
return { patients, nextCursor, hasMore }
}
Real-Time Data Pattern
Subscription Setup
export function useRealtimePatients() {
const [patients, setPatients] = useState<Patient[]>([])
useEffect(() => {
// Initial fetch
getPatients().then(setPatients)
// Subscribe to changes
const subscription = supabase
.channel('patients')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'patients' },
(payload) => {
if (payload.eventType === 'INSERT') {
setPatients(prev => [payload.new as Patient, ...prev])
} else if (payload.eventType === 'UPDATE') {
setPatients(prev =>
prev.map(p => p.id === payload.new.id ? payload.new : p)
)
} else if (payload.eventType === 'DELETE') {
setPatients(prev =>
prev.filter(p => p.id !== payload.old.id)
)
}
}
)
.subscribe()
return () => {
subscription.unsubscribe()
}
}, [])
return patients
}
These patterns ensure consistent, type-safe, and efficient data handling throughout the application.