Skip to main content

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.