Medplum (FHIR healthcare) Rules
These rules come from app/rules/medplum/ in ai-toolkit. They cover
the project's standards for coding style, frameworks, patterns,
security, and testing in Medplum (FHIR healthcare). Apply them when writing or
reviewing Medplum (FHIR healthcare) code.
Medplum / FHIR Coding Style
Resource Structure
- Every FHIR object must include
resourceTypeas first field. - Use PascalCase for resource types (
Patient,ServiceRequest), camelCase for fields (birthDate,valueQuantity). - Never hardcode resource IDs. Let the server assign them on create.
- Include
meta.profilewhen creating resources that must conform to a StructureDefinition.
References
- Use
createReference(resource)from@medplum/coreto build Reference objects. - Always include
displayon references for human readability. - Use
getReferenceString(resource)for comparisons and logging — returnsResourceType/id. - Use
parseReference(ref)to extract resourceType and id from a reference string. - Never concatenate strings to build references manually.
CodeableConcepts & Coding
- Always include
system,code, anddisplayin every Coding element. - Use standard terminology URIs:
http://loinc.org,http://snomed.info/sct,http://hl7.org/fhir/sid/icd-10-cm. - Use
getCodeBySystem(cc, system)to find codes;setCodeBySystem(cc, system, code)to set them. - Prefer
CodeableConceptover plainCodingwhen the FHIR spec allows both — it supports multiple codings and free text.
Identifiers
- Use
identifierarrays withsystem+valuefor external IDs (MRN, NPI, SSN). - Use
getIdentifier(resource, system)andsetIdentifier(resource, system, value)helpers. - Identifier systems must be absolute URIs (e.g.,
http://hl7.org/fhir/sid/us-npi). - Use
createResourceIfNoneExist(resource, 'identifier=system|value')for idempotent creates.
Extensions
- Use the
extensionarray withurland typedvalue[x]fields. - Prefer official HL7/US Core extensions over custom ones where they exist.
- Use
getExtension(resource, url)andgetExtensionValue(resource, url)helpers.
Bundles
- Use
urn:uuid:<uuid>for internal references between entries in a transaction Bundle. - Every Bundle entry must have
request.method(POST,PUT,DELETE) andrequest.url. - Include
fullUrlon entries that are referenced by other entries. - Use conditional references (
Practitioner?identifier=npi|123) for existing resources.
HIPAA-Aware Coding
- Identifiers like SSN, MRN, and insurance IDs are PHI — never log raw values to console or external services.
- Use a safe logging utility (e.g.,
safeLog()) for any output that might contain patient data. Neverconsole.lograw FHIR resources. - Reference
displaystrings may contain patient names — treat as PHI in logs and error messages. - Every new data access path or admin operation must include corresponding
AuditEventcreation. No exceptions. - When audit logging fails (Medplum unreachable), write to a fallback store — audit events must never be silently dropped.
- See
security.mdrules for full HIPAA, access policy, and PHI handling requirements.
Formatting
- Use
formatHumanName(),formatAddress(),formatDate(),formatQuantity()for display strings. - Use
getDisplayString(resource)as a universal fallback for any resource's display name. - Never manually concatenate name parts — FHIR names have
given[],family,prefix[],suffix[].
Medplum Frameworks
@medplum/core — SDK Client
- Use
MedplumClientfor all FHIR operations. Never use rawfetchagainst Medplum endpoints. - Use
medplum.createResource(),readResource(),updateResource(),deleteResource()for CRUD. - Use
medplum.searchResources()for typed arrays. Usemedplum.searchOne()when expecting a single result. - Use
medplum.executeBatch()for transaction Bundles — groups multiple operations atomically. - Use
medplum.upsertResource(resource, query)for atomic create-or-update. - Use
medplum.createResourceIfNoneExist(resource, query)for idempotent creation. - Configure
autoBatchTimeon MedplumClient to auto-batch concurrent GET requests. UsePromise.all()instead of sequentialawaitto benefit from batching.
@medplum/fhirtypes — Type Safety
- Import FHIR types directly:
import { Patient, Observation } from '@medplum/fhirtypes'. - Use TypeScript types for all FHIR resources — never use
anyfor resource data. - Cast
event.inputin bot handlers:const patient = event.input as Patient. - Use optional chaining for nested FHIR fields:
patient.name?.[0]?.given?.[0].
@medplum/react — UI Components
- Wrap app with
<MedplumProvider client={medplum}>at the root. - Use
useMedplum()hook to access the MedplumClient instance in components. - Use
useMedplumContext()for client + profile + loading state together. - Use
<ResourceForm>for auto-generated CRUD forms,<ResourceTable>for display. - Use
<SearchControl>for searchable/filterable resource lists. - Use
<QuestionnaireForm>to render FHIR Questionnaires and capture responses. - Use
useSubscription(criteria)for real-time WebSocket data in React components. - Requires Mantine 7+ and PostCSS with Mantine preset. Import
@mantine/core/styles.css.
Bot Development
- Export a single
handlerfunction:export async function handler(medplum: MedplumClient, event: BotEvent). - Access trigger resource via
event.input. Access secrets viaevent.secrets. - Use
event.contentTypeto determine input format (application/fhir+json,text/plain,x-application/hl7-v2+er7). - Deploy bots via CLI for CI/CD:
medplum bot deploy <bot-name>. - Apply AccessPolicies to bots — restrict to minimum required resource types.
- Use Subscriptions with
channel.type: 'rest-hook'andchannel.endpoint: 'Bot/<ID>'for event-driven execution.
GraphQL
- Append
Listto resource type for searches:PatientList(name: "Eve"). - Use snake_case for search parameters in GraphQL (not kebab-case):
address_city, notaddress-city. - Use inline fragments for reference resolution:
... on Observation { valueQuantity { value } }. - Use
_referencefor reverse lookups:EncounterList(_reference: patient).
CLI (@medplum/cli)
- Use
medplum loginfor auth,medplum get/medplum postfor FHIR operations. - Use
medplum bot deployfor bot deployment in CI/CD pipelines. - Use
medplum bulk exportfor bulk data operations.
Medplum / FHIR Patterns
Bundle Transactions
- Use
type: 'transaction'for atomic multi-resource operations — all-or-nothing. - Use
type: 'batch'when operations are independent and partial failure is acceptable. - Use
urn:uuid:<uuid>infullUrlfor forward references between entries. - Reference other entries via
{ reference: 'urn:uuid:<uuid>' }. - Use
ifNoneExiston POST entries for conditional creation (idempotent). - Use
ifMatch: 'W/"versionId"'on PUT entries for optimistic concurrency. - Use conditional references for existing resources:
Practitioner?identifier=http://hl7.org/fhir/sid/us-npi|123. - For large bundles (>50MB), use
Prefer: respond-asyncheader.
Search Patterns
- Use
_include=ResourceType:searchParamto fetch referenced resources in one call. - Use
_revinclude=ResourceType:searchParamto fetch resources referencing your results. - Use
:iteratemodifier for multi-hop traversal:_include:iterate=Patient:general-practitioner. - Use
_count+_offsetfor pagination; usesearchResourcePages()for async iteration. - Use
:containsmodifier for substring search on string params:name:contains=eve. - Use
:notmodifier to exclude:status:not=completed. - Use comma-separated values for OR:
status=active,on-hold. - Use multiple parameters for AND:
name=Simpson&birthdate=1940-03-29. - Prefer
searchResources()oversearch()— returns typed array, not raw Bundle.
Subscription & Bot Workflows
- Create a
Subscriptionresource withcriteria(FHIR search query) andchannel.type: 'rest-hook'. - Point
channel.endpointtoBot/<BOT_ID>for automated processing. - Use
subscribeToCriteria()client-side for WebSocket real-time updates. - Never subscribe to
AuditEventchanges — prevents notification spirals. - Use cron-based bots for scheduled tasks (e.g., daily reports, batch processing).
Access Policies
- Define
AccessPolicy.resource[]withresourceTypeand optionalcriteria,readonly,hiddenFields,readonlyFields. - Use
%profilevariable to scope data to the current user:Observation?performer=%profile. - Use
%patientvariable for patient-portal access:Observation?subject=%patient. - Use compartment-based access for patient-scoped isolation.
- Use
writeConstraintwith FHIRPath for state machine enforcement (e.g., prevent status rollback). - Apply least privilege: start with no access, add specific resource types.
Conditional Operations
- Use
createResourceIfNoneExist(resource, query)for idempotent creates keyed on identifier. - Use
upsertResource(resource, query)for atomic create-or-update in a single request. - Use
If-None-Existheader on POST for server-side conditional creation.
Patient Deduplication
- Match on
identifiersystems (MRN, SSN, insurance ID) for deterministic matching. - Use probabilistic matching on name + birthdate + address for fuzzy matches.
- Use Patient
linkfield withtype: 'replaced-by'for merge workflows. - Prefer
createResourceIfNoneExist()at ingestion to prevent duplicates.
Questionnaire Workflows
- Create
Questionnaireresources for form definitions. UselinkIdfor question identification. - Use
QuestionnaireResponsefor captured answers. Link to Questionnaire viaquestionnairefield. - Use
getQuestionnaireAnswers(response)to extract answers as a map keyed bylinkId. - Automate post-submission processing with a Bot subscribed to
QuestionnaireResponsecreation. - Use SDC (Structured Data Capture) extensions for advanced rendering and extraction.
Medplum / FHIR Security
Authentication
- Use client credentials flow (
startClientLogin) for backend services and integrations. - Use authorization code flow (
startLogin+processCode) for user-facing web apps. - Never store tokens in
localStoragein production — use secure HTTP-only cookies or server-side sessions. - Use
refreshIfExpired()before critical operations. SetgracePeriodto refresh proactively. - Use
setBasicAuth(clientId, clientSecret)only for server-side code, never in browser. - Rotate client secrets via
$rotate-client-secretoperation periodically.
Access Policies
- Every non-admin user must have an AccessPolicy. Never leave users with default full access.
- Scope to specific resource types: list only the types the user needs.
- Use
readonly: trueor explicitinteractionarrays to restrict write access. - Use
criteriawith FHIR search syntax to filter visible resources (e.g.,Patient?organization=Organization/123). - Use
hiddenFieldsto prevent sensitive fields from being returned (e.g., SSN). - Use
readonlyFieldsto allow viewing but prevent modification of specific fields. - Use
writeConstraintFHIRPath expressions for business rules (e.g., prevent status rollback on finalized resources). - Test access policies by logging in as a test user with the policy applied.
HIPAA & Audit Logging
- Medplum automatically creates AuditEvent resources for all FHIR operations.
- Never log PHI (patient names, identifiers, health data) to application console or external services.
- Use structured audit references: reference the Patient and the accessing Practitioner in audit records.
- For custom audit trails, create AuditEvent resources with
type,agent,entity, andoutcome. - Ensure audit events are never silently dropped — if Medplum is unreachable, write to a fallback store.
PHI Handling
- Never include PHI in URLs, query parameters, or HTTP headers.
- Use POST-based search for queries containing sensitive criteria.
- Use
Binaryresources withsecurityContextfor sensitive file attachments. - Encrypt data at rest and in transit (TLS 1.2+). Medplum hosted handles this automatically.
- Apply data retention policies — use
$expungeoperation for permanent deletion when required.
SMART Scopes
- Use
patient/*.readstyle scopes for patient-facing apps. - Use
user/*.readstyle scopes for practitioner-facing apps. - Validate scopes server-side on every request — do not trust client-side scope claims.
- Use
launch/patientcontext for apps launched within a patient context. - Define minimal scopes: request only the resource types and operations needed.
Multi-Tenant Isolation
- Use Medplum Projects for tenant isolation — each project is a separate data silo.
- Never share AccessPolicies across projects/tenants.
- Validate
meta.projecton operations when building multi-tenant middleware. - Use separate ClientApplications per tenant for backend integrations.
Secrets Management
- Use Bot secrets (
event.secrets) for API keys, connection strings, and credentials. - Never hardcode secrets in bot source code or resource data.
- Use Medplum project-level secrets storage — accessible only by project admins.
- Rotate secrets on a regular schedule and after any suspected compromise.
Medplum / FHIR Testing
MockClient
- Use
MockClientfrom@medplum/mockfor unit tests — it simulates the full MedplumClient API in memory. - Pre-populate test data with
mockClient.createResource()before running test assertions. - MockClient supports
search,searchResources,searchOne,readResource,updateResource,deleteResource. - MockClient does not require network access — tests run fast and offline.
- Use
new MockClient()per test to ensure isolation between test cases.
Bot Unit Testing
- Test the handler function directly:
await handler(mockClient, mockEvent). - Create mock
BotEventobjects withinput,contentType,secrets, andbotfields. - Verify resource creation: call
mockClient.searchResources()after handler execution. - Test error paths: pass invalid input resources and assert the handler throws or returns errors.
- Test different content types:
application/fhir+json,text/plain,x-application/hl7-v2+er7. - Mock
event.secretsfor bots that depend on external API keys.
Resource Validation
- Use
validateResource(resource)from@medplum/coreto check resources against StructureDefinitions. - Test that required fields produce
OperationOutcomeerrors when missing. - Test custom profiles: create a
StructureDefinitionresource, then validate resources against it. - Use the
$validateoperation for server-side validation in integration tests. - Test the Data Absent Reason extension when required fields may legitimately be empty.
Search Testing
- Test search parameter behavior: exact match vs prefix match vs substring (
name,name:exact,name:contains). - Verify
_includereturns related resources in the Bundle. - Test pagination with
_countand_offsetparameters. - Test token search with and without system namespace:
identifier=valuevsidentifier=system|value. - Test date range searches with comparison prefixes:
ge,le,gt,lt.
Integration Testing
- Use Medplum Docker image (
medplum/medplum-server) for local integration tests. - Test full workflows end-to-end: create patient → create observation → search → verify.
- Verify Bundle transactions are atomic: intentionally fail one entry and confirm rollback.
- Test access policies by authenticating as users with different policies.
- Test Subscription triggers: create a Subscription, modify a matching resource, verify Bot execution.
Test Data Factories
- Create typed factory functions:
createTestPatient(overrides?),createTestObservation(overrides?). - Use realistic but synthetic data — never use real patient data in tests.
- Include only minimal required fields by default. Let tests add specific fields via overrides.
- Use
generateId()from@medplum/corefor unique test identifiers. - Use standard test identifier systems:
http://example.com/test-mrnto avoid collision with real systems.
Assertions
- Assert on
OperationOutcomefor error responses: checkissue[].severity,issue[].code,issue[].expression. - Use
isOk(outcome)andisNotFound(outcome)from@medplum/corefor status checks. - Use
deepEquals(a, b)for resource comparison (ignoresmeta.versionIdandmeta.lastUpdated). - Assert reference integrity: verify
subject.referencematches expectedPatient/idformat.