Skip to main content

FHIR Batch Requests

FHIR allows users to create batch requests to bundle multiple API calls into a single HTTP request. Batch requests can improve speed and efficiency and can reduce HTTP traffic when working with many resources.

Cloning a project

If you want to create a copy of a project, say for a new environment, this can be done using the $clone operation rather than by creating a batch request. For more details see the Projects guide.

How to Perform a Batch Request

Batch requests are modeled using the Bundle resource by setting Bundle.type to "batch".

Batch requests are performed by sending a POST request to [baseURL]/ with a FHIR Bundle. The Medplum SDK provides the executeBatch helper function to simplify this operation.

The details of your request will be in the entry field of the Bundle, which is an array of BundleEntry objects. Each BundleEntry should have the details of the resource you are working with, as well as additional information about the request you are making.

ElementDescription
request.urlThe URL to send your request to. This is relative to the base R4 FHIR URL (e.g. https://api.medplum.com/fhir/R4).
request.methodThe type of HTTP request you are making. Can be one of the following:
  • GET: Read a resource or perform a search
  • POST: Create a resource
  • PUT: Update a resource
  • DELETE: Delete a resource
request.ifNoneExistSee below
resourceThe details of the FHIR resource that is being created/updated.
fullUrlSee below
Example: A simple batch request to simultaneously search for two patients
await medplum.executeBatch({
resourceType: 'Bundle',
type: 'batch',
entry: [
{
request: {
method: 'GET',
url: 'Patient/homer-simpson',
},
},
{
request: {
method: 'GET',
url: 'Patient/marge-simpson',
},
},
],
});
Example: Create multiple resources in one batch request
{
resourceType: 'Bundle',
type: 'batch',
entry: [
{
resource: {
resourceType: 'Patient',
identifier: [
{
system: 'https://example-org.com/patient-ids',
value: 'homer-simpson',
},
],
name: [
{
family: 'Simpson',
given: ['Homer', 'Jay'],
},
],
},
request: {
method: 'POST',
url: 'Patient',
},
},
{
resource: {
resourceType: 'Patient',
identifier: [
{
system: 'https://example-org.com/patient-ids',
value: 'marge-simpson',
},
],
name: [
{
family: 'Simpson',
given: ['Marge', 'Jacqueline'],
},
],
},
request: {
method: 'POST',
url: 'Patient',
},
},
],
};
Example: Make multiple calls to the _history endpoint in one batch request
{
resourceType: 'Bundle',
type: 'batch',
entry: [
{
request: {
method: 'GET',
url: 'Patient/homer-simpson/_history',
},
},
{
request: {
method: 'GET',
url: 'Patient/marge-simpson/_history',
},
},
{
request: {
method: 'GET',
url: 'Organization/_history',
},
},
],
};
Batch vs Transaction

Medplum does not currently distinguish between 'batch' and 'transaction' type Bundle resources and does not provide atomic transactions. This is currently being worked on, and you can track progress on the issue here.

Asynchronous Batch Requests

Processing very large batches (for example, a Bundle over 5 MB in size) can take a significant amount of time, often longer than the default request timeout of one minute. This causes issues with processing the batch, since an error is returned to the client before they can access the actual results of the batch request. To resolve this problem, Medplum offers the ability to process the batch asynchronously: a large Bundle (to to 50 MB by default) can be sent to the server, where it will be enqueued for processing, and a response is sent back to the client immediately with a URL where it can receive status updates around the batch processing job.

To opt into asynchronous handling of the batch request, add the Prefer: respond-async header to the HTTP request for the batch:

curl 'https://api.medplum.com/fhir/R4' \
-X POST
-H 'authorization: Bearer $ACCESS_TOKEN' \
-H 'content-type: application/fhir+json' \
-H 'prefer: respond-async'
-d @large-bundle.json

See the FHIR Asynchronous Request pattern for more information.

References in Bundles

When using batch requests, it is common to require resources to refer to one another: either between two resources within the Bundle, or to resources outside the Bundle. FHIR provides ways to automatically link the resources together, without needing to know the exact resource IDs up front.

Internal References

Resources within the same Bundle often need to link to one another: for example, you may create a Patient resource that is the subject of an Encounter created in the same batch request.

Creating internal references is done by assigning temporary IDs to each bundle entry. The fullUrl field is set to 'urn:uuid:' followed by a temporary UUID.

Other bundle entries can refer to this resource using the temporary urn:uuid: string, which is replaced by the real reference during batch processing.

Example: Create a patient and encounter whose subject is the created patient
{
resourceType: 'Bundle',
type: 'transaction',
entry: [
{
fullUrl: 'urn:uuid:f7c8d72c-e02a-4baf-ba04-038c9f753a1c',
resource: {
resourceType: 'Patient',
name: [
{
prefix: ['Ms.'],
family: 'Doe',
given: ['Jane'],
},
],
gender: 'female',
birthDate: '1970-01-01',
},
request: {
method: 'POST',
url: 'Patient',
},
},
{
fullUrl: 'urn:uuid:7c988bc7-f811-4931-a166-7c1ac5b41a38',
resource: {
resourceType: 'Encounter',
status: 'finished',
class: { code: 'ambulatory' },
subject: {
reference: 'urn:uuid:f7c8d72c-e02a-4baf-ba04-038c9f753a1c',
display: 'Ms. Jane Doe',
},
type: [
{
coding: [
{
system: 'http://snomed.info/sct',
code: '162673000',
display: 'General examination of patient (procedure)',
},
],
},
],
},
request: {
method: 'POST',
url: 'Encounter',
},
},
],
};

External (Conditional) References

Additionally, it may be necessary to link to resources that already exist on the server outside of the Bundle, for which the ID of the target resource is not known. To handle this use case, FHIR allows populating the reference with a search query string, which will be resolved by Medplum server at resource creation time. This search must resolve to a single resource, otherwise an error will be returned. For example:

Example: Create multiple patients that link to the same provider
{
resourceType: 'Bundle',
type: 'transaction',
entry: [
{
resource: {
resourceType: 'Patient',
name: [
{
prefix: ['Ms.'],
family: 'Doe',
given: ['Jane'],
},
],
gender: 'female',
birthDate: '1970-01-01',
generalPractitioner: [{ reference: 'Practitioner?identifier=http://hl7.org/fhir/sid/us-npi|1234567893' }],
},
request: {
method: 'POST',
url: 'Patient',
},
},
{
resource: {
resourceType: 'Patient',
name: [
{
prefix: ['Mr.'],
family: 'Doe',
given: ['John'],
},
],
gender: 'male',
birthDate: '1972-12-31',
generalPractitioner: [{ reference: 'Practitioner?identifier=http://hl7.org/fhir/sid/us-npi|1234567893' }],
},
request: {
method: 'POST',
url: 'Patient',
},
},
],
};

Conditional Batch Actions

There may be situations where you would only like to create a a resource as part of a batch request if it does not already exist.

You can conditionally perform batch actions by adding the ifNoneExist property to the request element of your Bundle.

The ifNoneExist property uses search parameters to search existing resources and only performs the action if no match is found. Since you are already defining the url to send the request to, you only need to enter the actual parameter in this field (i.e., everything that would come after the ? when submitting an actual search).

Example: Create a patient and organization, only if the organization does not already exist
{
resourceType: 'Bundle',
type: 'transaction',
entry: [
{
fullUrl: 'urn:uuid:4aac5fb6-c2ff-4851-b3cf-d66d63a82a17',
resource: {
resourceType: 'Organization',
identifier: [
{
system: 'http://example-org.com/organizations',
value: 'example-organization',
},
],
name: 'Example Organization',
},
request: {
method: 'POST',
url: 'Organization',
ifNoneExist: 'identifier=https://example-org.com/organizations|example-organization',
},
},
{
fullUrl: 'urn:uuid:37b0dfaa-f320-444f-b658-01a04985b2ce',
resource: {
resourceType: 'Patient',
name: [
{
use: 'official',
family: 'Smith',
given: ['Alice'],
},
],
gender: 'female',
birthDate: '1974-12-15',
managingOrganization: {
reference: 'urn:uuid:4aac5fb6-c2ff-4851-b3cf-d66d63a82a17',
display: 'Example Organization',
},
},
request: {
method: 'POST',
url: 'Patient',
},
},
],
};

Performing Upserts

Previously, performing an "upsert" (i.e. either creating or updating a resource based on whether it already exists) required using a batch operation. This functionality is now implemented directly as a conditional update to provide strong transactional guarantees around the operation in a single, simple PUT request.

Upsert URLs

Because resolving the search request in the upsert URL is a prerequisite to determine which ID should be assigned as part of handling internal references, the urn:uuid: syntax for internal references cannot be used in the Bundle.entry.request.url for a conditional update within a batch.

Instead, upsert URLs should link to other resources within the Bundle consistently through conditions on the target resource itself, not its server-assigned ID.

Example

Incorrect

{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"fullUrl": "urn:uuid:4410cb87-4a38-4d3f-bee8-3c3556e6debc",
"request": {
"method": "PUT",
"url": "Patient?identifier=http://example.com/mrn|1234567"
},
"resource": {
"resourceType": "Patient",
"identifier": [{ "system": "http://example.com/mrn", "value": "1234567" }]
}
},
{
"fullUrl": "urn:uuid:b122a53c-9c0d-4654-9260-c0b67b7bc5d4",
"request": {
"method": "PUT",
// Incorrect: Upsert URL cannot use replaced urn:uuid: syntax
"url": "CareTeam?subject=urn:uuid:4410cb87-4a38-4d3f-bee8-3c3556e6debc"
},
"resource": {
"resourceType": "CareTeam",
"subject": { "reference": "urn:uuid:4410cb87-4a38-4d3f-bee8-3c3556e6debc" }
}
}
]
}

Correct

{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"fullUrl": "urn:uuid:4410cb87-4a38-4d3f-bee8-3c3556e6debc",
"request": {
"method": "PUT",
"url": "Patient?identifier=http://example.com/mrn|1234567"
},
"resource": {
"resourceType": "Patient",
"identifier": [{ "system": "http://example.com/mrn", "value": "1234567" }]
}
},
{
"fullUrl": "urn:uuid:b122a53c-9c0d-4654-9260-c0b67b7bc5d4",
"request": {
"method": "PUT",
// Correct: Refer to subject via chained search,
// using same identifier as its own upsert URL
"url": "CareTeam?subject.identifier=http://example.com/mrn|1234567"
},
"resource": {
"resourceType": "CareTeam",
"subject": { "reference": "urn:uuid:4410cb87-4a38-4d3f-bee8-3c3556e6debc" }
}
}
]
}

PATCH actions in a Batch

Because the request body of a PATCH request is an array of JSON Patch operations, not a resource, some extra conversion is required to represent these actions in a batch request. The FHIR recommended pattern for this use case is to base64 encode the JSON Patch array and represent it as a Binary resource:

{
"request": {
"method": "PATCH",
"url": "Patient/6ffaaab4-ff7e-4416-80c7-8fce95c3e31c"
},
"resource": "resource": {
"resourceType": "Binary",
"contentType": "application/json-patch+json",
// Encoded: [{"op":"test","path":"/active","value":false}]
"data": "W3sib3AiOiJyZXBsYWNlIiwicGF0aCI6Ii9hY3RpdmUiLCJ2YWx1ZSI6ZmFsc2V9XQo="
},
}

Additionally, Medplum supports passing JSON Patch formatted as a Parameters resource, making it easier to see what operations are happening in the batch.

Value Formatting

To avoid parsing ambiguity and simplify the format of the Parameters, all values are passed as JSON strings, via the valueString field on each corresponding Parameter.parameter.part with "name": "value".

All types of values, including booleans, strings, objects, and arrays, should be passed to JSON.stringify() or equivalent rather than being included directly in the Parameters.

{
"request": {
"method": "PATCH",
"url": "Patient/6ffaaab4-ff7e-4416-80c7-8fce95c3e31c"
},
"resource": {
"resourceType": "Parameters",
"parameter": [
{
"name": "operation",
"part": [
{ "name": "op", "valueCode": "test" },
{ "name": "path", "valueString": "/active" },
{ "name": "value", "valueString": "false" }
]
},
{
"name": "operation",
"part": [
{ "name": "op", "valueCode": "add" },
{ "name": "path", "valueString": "/name/-" },
// Note that values must be JSON encoded
{ "name": "value", "valueString": "{\"given\":[\"Dave\"],\"family\":\"Smith\"}" }
]
}
]
}
}

Medplum Autobatching

The Medplum Client provides the option to automatically batch FHIR read and search requests using the autoBatchTime parameter. This field allows you to set a time window during which to batch up any GET requests. After this window expires, the MedplumClient will add them to a Bundle behind the scenes and then execute them as a batch request.

Autobatching works by creating a queue of Promises issued within the autoBatchTime window and then creating a bundle out of these requests. To allow the queue to be created, you must make sure that the main thread continues to run, so you should not use await after each request. Using await will pause the main thread each time a request is made, so a queue cannot be created.

Instead you should create the queue of Promise requests and then use Promise.all() to resolve all of them at once.

Details

Resolving Promises with autobatching❌ WRONG

// Main thread pauses and waits for Promise to resolve. This request cannot be added to a batch
await medplum.createResource({
resourceType: 'Patient',
name: [
{
family: 'Smith',
given: ['John'],
},
],
});

// Main thread pauses and waits for Promise to resolve. This request cannot be added to a batch
await medplum.createResource({
resourceType: 'Patient',
name: [
{
family: 'Simpson',
given: ['Homer', 'Jay'],
},
],
});

✅ CORRECT

const patientsToCreate = [];

// Main thread continues
patientsToCreate.push(
medplum.createResource({
resourceType: 'Patient',
name: [
{
family: 'Smith',
given: ['John'],
},
],
})
);

// Main thread continues
patientsToCreate.push(
medplum.createResource({
resourceType: 'Patient',
name: [
{
family: 'Simpson',
given: ['Homer', 'Jay'],
},
],
})
);

// Both promises are resolved simultaneously
await Promise.all(patientsToCreate);