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.
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.
Element | Description |
---|---|
request.url | The URL to send your request to. This is relative to the base R4 FHIR URL (e.g. https://api.medplum.com/fhir/R4). |
request.method | The type of HTTP request you are making. Can be one of the following:
|
request.ifNoneExist | See below |
resource | The details of the FHIR resource that is being created/updated. |
fullUrl | See below |
Example: A simple batch request to simultaneously search for two patients
- Typescript
- CLI
- cURL
await medplum.executeBatch({
resourceType: 'Bundle',
type: 'batch',
entry: [
{
request: {
method: 'GET',
url: 'Patient/homer-simpson',
},
},
{
request: {
method: 'GET',
url: 'Patient/marge-simpson',
},
},
],
});
medplum post Bundle '
{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": {
"method": "GET",
"url": "Patient/homer-simpson",
},
},
{
"request": {
"method": "GET",
"url": "Patient/marge-simpson",
},
},
],
}'
curl 'https://api.medplum.com/fhir/R4' \
-X POST
-H 'authorization: Bearer $ACCESS_TOKEN' \
-H 'content-type: application/fhir+json' \
-d '{
"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',
},
},
],
};
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
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.
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.
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);