This file provides guidance for AI coding agents working on Medplum projects.
- TypeScript with strict mode
- Single quotes, trailing commas (ES5 style)
- Prettier with 120 character line width
- ESLint for linting
- Use functional React components with hooks
- Mantine UI library for components (@mantine/core)
IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any Mantine UI tasks.
Prefer Mantine's built-in style props over inline style={} attributes for better maintainability and theme consistency.
Common Style Props:
import { Box, Stack, Group } from '@mantine/core';
// ✅ Good: Use Mantine props
<Box
p="md" // padding
m="lg" // margin
bg="gray.0" // background
c="blue.6" // color
bd="1px solid #dee" // border
w={300} // width
h={200} // height
mih={100} // minHeight
maw={500} // maxWidth
/>
// ❌ Bad: Inline styles
<Box style={{
padding: '16px',
margin: '24px',
background: '#f8f9fa',
minHeight: '100px'
}} />
// ✅ Use Stack/Group for flex layouts instead of manual flex styles
<Stack align="center" justify="center" h="100%">
<Text>Centered content</Text>
</Stack>
// ❌ Bad: Manual flex with inline styles
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Text>Centered content</Text>
</div>When Inline Styles Are Acceptable:
zIndexfor stacking control (e.g.,style={{ zIndex: 10 }})borderTop/borderBottomwhen not available as props (e.g.,style={{ borderTop: '1px solid #dee' }})- Complex CSS that doesn't have Mantine equivalents
- One-off adjustments (use sparingly)
Replace HTML Elements:
// ✅ Good: Use Box instead of div with styles
<Box p="xs" bd="1px solid #e9ecef" style={{ borderRadius: 8 }}>
<Text>Content</Text>
</Box>
// ❌ Bad: div with inline styles
<div style={{ padding: '12px', border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Text>Content</Text>
</div>Resources: Core FHIR data objects (Patient, Observation, MedicationRequest, etc.). Every resource has:
resourceType: String identifying the typeid: Server-assigned unique identifiermeta: Metadata includingversionIdandlastUpdated
References: Links between resources using { reference: "ResourceType/id" } format.
CodeableConcepts: Standardized codes with { coding: [{ system, code, display }], text } structure.
Identifiers: Multiple IDs for the same entity using { system, value } pairs.
| Resource | Purpose |
|---|---|
Patient |
Patient demographics |
Practitioner |
Healthcare provider |
Observation |
Clinical measurements (vitals, lab results) |
Condition |
Diagnoses |
MedicationRequest |
Prescriptions |
DiagnosticReport |
Lab reports |
Communication |
Messages |
Task |
Workflow items |
CareTeam |
Care team members |
CarePlan |
Treatment plans |
ServiceRequest |
Orders for procedures/labs |
Questionnaire |
Forms/surveys |
QuestionnaireResponse |
Form submissions |
- LOINC (
http://loinc.org): Lab tests, observations - SNOMED CT (
http://snomed.info/sct): Clinical terms - ICD-10 (
http://hl7.org/fhir/sid/icd-10): Diagnoses - RxNorm (
http://www.nlm.nih.gov/research/umls/rxnorm): Medications - CPT (
http://www.ama-assn.org/go/cpt): Procedures/billing
IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any Medplum SDK tasks.
import { MedplumClient } from '@medplum/core';
const medplum = new MedplumClient({
baseUrl: 'https://api.medplum.com/',
});
// Or in React components:
import { useMedplum } from '@medplum/react';
const medplum = useMedplum();// Create
const patient = await medplum.createResource<Patient>({
resourceType: 'Patient',
name: [{ given: ['John'], family: 'Doe' }],
});
// Read
const patient = await medplum.readResource('Patient', 'patient-id');
// Update
await medplum.updateResource({ ...patient, birthDate: '1990-01-01' });
// Delete
await medplum.deleteResource('Patient', 'patient-id');
// Upsert (create or update based on identifier)
await medplum.upsertResource(patient, 'identifier=mrn-12345');
// Conditional create (only create if not exists)
await medplum.createResourceIfNoneExist(patient, 'identifier=mrn-12345');import { getReferenceString, createReference } from '@medplum/core';
// Basic search - returns Bundle
const bundle = await medplum.search('Patient', { name: 'Simpson' });
// Search resources - returns array of resources
const patients = await medplum.searchResources('Patient', { name: 'Simpson' });
// Search by reference
const observations = await medplum.searchResources('Observation', {
subject: getReferenceString(patient), // "Patient/123"
});
// Multiple values (OR)
const tasks = await medplum.searchResources('Task', {
status: 'completed,cancelled',
});
// Multiple parameters (AND)
const results = await medplum.searchResources('Patient', {
name: 'Simpson',
birthdate: '1990-01-01',
});
// Search modifiers
await medplum.searchResources('Task', { 'status:not': 'completed' });
await medplum.searchResources('Patient', { 'birthdate:missing': 'true' });
await medplum.searchResources('Patient', { 'name:contains': 'eve' });
// Comparisons for numbers/dates
await medplum.searchResources('RiskAssessment', { probability: 'gt0.8' });
await medplum.searchResources('Observation', { 'value-quantity': 'gt40' });
// Sorting
await medplum.searchResources('Patient', { _sort: '-_lastUpdated' });
// Pagination
await medplum.searchResources('Patient', { _count: '50', _offset: '100' });
// Paginated iteration
for await (const page of medplum.searchResourcePages('Patient', { _count: 10 })) {
for (const patient of page) {
console.log(patient.id);
}
}// Search observations by patient name
await medplum.searchResources('Observation', {
'patient.name': 'homer',
});
// With resource type qualifier
await medplum.searchResources('Observation', {
'subject:Patient.name': 'homer',
});
// Reverse chained search (_has)
// Find patients who have observations with a specific code
await medplum.searchResources('Patient', {
'_has:Observation:subject:code': '8867-4',
});// _include: Include referenced resources
await medplum.searchResources('Observation', {
code: '78012-2',
_include: 'Observation:patient', // Include Patient referenced by Observation
});
// _revinclude: Include resources that reference results
await medplum.searchResources('Patient', {
_id: 'patient-123',
_revinclude: 'Observation:subject', // Include Observations referencing Patient
});
// :iterate for multiple hops
await medplum.searchResources('Observation', {
code: '78012-2',
_include: 'Observation:patient',
'_include:iterate': 'Patient:general-practitioner',
});import { createReference, getReferenceString } from '@medplum/core';
// Create a reference object
const patientRef = createReference(patient);
// { reference: 'Patient/123', display: 'John Doe' }
// Get reference string only
const refString = getReferenceString(patient);
// 'Patient/123'
// Use in a resource
const observation: Observation = {
resourceType: 'Observation',
status: 'final',
code: { text: 'Heart Rate' },
subject: createReference(patient),
performer: [createReference(practitioner)],
};import { MedplumClient, formatDate, getReferenceString, createReference } from '@medplum/core';
import type { Patient, Observation } from '@medplum/fhirtypes';
import {
useMedplum,
useMedplumProfile,
useResource,
useSearch,
Document,
ResourceTable,
PatientTimeline,
SearchControl
} from '@medplum/react';// Get the MedplumClient
const medplum = useMedplum();
// Get current user profile
const profile = useMedplumProfile();
// Fetch a single resource
const patient = useResource<Patient>({ reference: 'Patient/123' });
// Search with automatic updates
const observations = useSearch('Observation', { subject: 'Patient/123' });Use useState and useEffect with async/await for data fetching (NOT Suspense with .read()). Prefer a single isLoading flag plus a cancellation guard:
import { useEffect, useState } from 'react';
import { useMedplum, useMedplumProfile, ErrorBoundary } from '@medplum/react';
import { getReferenceString } from '@medplum/core';
import type { Observation, Patient } from '@medplum/fhirtypes';
function MyComponent() {
const medplum = useMedplum();
const patient = useMedplumProfile() as Patient | undefined;
const subject = patient ? getReferenceString(patient) : undefined;
const [observations, setObservations] = useState<Observation[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let cancelled = false;
// useMedplumProfile returns undefined while loading
if (!patient) {
setIsLoading(true);
return;
}
if (!subject) {
setObservations([]);
setIsLoading(false);
return;
}
const fetchData = async () => {
setIsLoading(true);
try {
const results = await medplum.searchResources('Observation', { subject });
if (!cancelled) {
setObservations(results);
}
} catch (error) {
console.error('Failed to fetch:', error);
if (!cancelled) {
setObservations([]);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
};
void fetchData();
return () => {
cancelled = true;
};
}, [medplum, patient, subject]);
if (!patient || isLoading) {
return <div>Loading...</div>;
}
return <div>{observations.length} observations</div>;
}
// Wrap with ErrorBoundary for error handling
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}Important notes:
useMedplumProfile()returnsundefinedwhile loading - it is NOT a Suspense hook- Do NOT use
.read()for Suspense - use async/await or Promises with useState/useEffect - Keep loading logic simple: a single
isLoadingstate and cancellation guard inuseEffect
Tests use Jest with jsdom environment. Mock the Medplum client:
import { MockClient } from '@medplum/mock';
import { MedplumProvider } from '@medplum/react';
const medplum = new MockClient();
// Preload test data
await medplum.createResource<Patient>({
resourceType: 'Patient',
id: 'test-patient',
name: [{ given: ['Test'], family: 'Patient' }],
});
// Render with provider
render(
<MedplumProvider medplum={medplum}>
<MyComponent />
</MedplumProvider>
);- Run
npm run lintandnpm testafter major changes - Update tests for any code changes
- Follow existing patterns in the codebase
- Use TypeScript types from
@medplum/fhirtypes