Disclaimer
This article is not meant to be a "GraphQL on FHIR" tutorial, but rather to show the benefits of considering it for your projects and offer another point of view.
I wanted to test how GraphQL behaves with FHIR, so I spent some time "playing" mainly on two servers:
- The server we use for the HL7 FHIR course taught by the Secretaría General del Sistema de la Integración Centroamericana (SG-SICA)
- The test server provided by HAPI FHIR
But before continuing and showing what I found, I feel obligated, for "general culture," to give a bit of history on how GraphQL made its way into HL7, specifically into FHIR.
Starting in 2019, HL7 decided to add support for the GraphQL standard to its FHIR R4 specification. GraphQL was originally created by Facebook (Meta) in 2012 as a response to the "over/under-fetching" problems that mobile apps were running into; the idea was to unify access to scattered data.
What exactly is GraphQL?
GraphQL is an open-source query language for APIs that lets clients request exactly the data they need, instead of receiving excessive or incomplete data. It's a more flexible and efficient alternative to RESTful APIs that solves some of their limitations, such as data overfetching. It runs on a strongly typed system that describes the data and lets clients define the shape of the response.
In 2015, Facebook published GraphQL as an open-source project along with a reference implementation in JavaScript, officially introducing the language. By 2018, the Linux Foundation announced the GraphQL Foundation to steward the specification and foster a neutral ecosystem, backed by major players in the industry. That same year, the SDL (Schema Definition Language) was also consolidated within the spec.
FHIR with GraphQL
In the FHIR R4 version, HL7 added the $graphql "operation" as an alternative to classic REST, as Trial Use, with the standard operations /$graphql and /{type}/{id}/$graphql. The formal $graphql operation is defined in the spec as an entry point to run it at the system or resource level.
As of 2025, GraphQL on FHIR is still not Normative; it remains Trial Use in the published version (R5). The standard's continuous build also shows it as Trial Use.
But even in its non-"Normative" state, I recommend using it, since, just like at Facebook, it avoids "over/under-fetching" and lets you traverse references in a single call — which is exactly what I want to show you.
Getting hands-on
I started playing with the servers from simple to more advanced, so I began with simple calls. I think those of us who've spent time getting our hands dirty with FHIR have our own "hello world": basically a query against the Patient resource. So I found a patient resource on the HL7 FHIR SG-SICA course server we use, with id 35, and fetched it:
GET http://r4.openfhir.org:8080/fhir/Patient/35
This query returned the following resource:
{
"resourceType": "Patient",
"id": "35",
"meta": {
"versionId": "1",
"lastUpdated": "2025-09-08T14:27:35.304+00:00"
},
"identifier": [
{
"use": "official",
"system": "https://minsal.gob.sv/fhir/patient-id",
"value": "paciente-AndresKury-SV"
}
],
"active": true,
"name": [
{
"use": "official",
"family": "Kury Rivas",
"given": [
"Andres Ernesto"
]
}
],
"telecom": [
{
"system": "phone",
"value": "+503 7555 5555",
"use": "mobile"
}
],
"gender": "male",
"birthDate": "1996-07-09",
"address": [
{
"use": "home",
"text": "San Salvador, El Salvador"
}
],
"contact": [
{
"relationship": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0131",
"code": "C",
"display": "Emergency Contact"
}
]
}
],
"name": {
"family": "Rivas",
"given": [
"Evelyn"
]
},
"telecom": [
{
"system": "phone",
"value": "+503 7111 1111"
}
]
}
]
}
The execution took 331 milliseconds, practically nothing, so I didn't expect much from such small queries — but I still wanted to test minimal payloads.
Now, here's where the magic of GraphQL kicks in: I'm not interested in getting all the information the FHIR server sends back, just specific data, and that's exactly what GraphQL lets me do:
query {
Patient(id: "35") {
id
name {
given
family
}
birthDate
gender
}
}
What I'm requesting here is that it return the id, full name (given and family), birth date and gender. Making this query, the FHIR server returns the following:
{
"data": {
"Patient": {
"id": "Patient/35/_history/1",
"name": [
{
"given": [
"Andres Ernesto"
],
"family": "Kury Rivas"
}
],
"birthDate": "1996-07-09",
"gender": "male"
}
}
}
As some of my friends would say, "beautiful." Unlike the GET query, this one took 98 milliseconds — that's 70% (3.38x faster) more efficient than the GET query.
Well, I moved on to something a bit more complex, so I looked for a way to query the patient's information along with their observations. When we talk about observations in FHIR, we're referring to:
- Vital signs
- Lab data
- Imaging results
- Clinical findings
- Device measurements
- Social history
Among other data. So the query for both resources in a "single shot" ended up like this:
GET http://r4.openfhir.org:8080/fhir/Patient/224/$everything?_type=Observation&_count=50
This query returned the following resource:
{
"resourceType": "Bundle",
"id": "88a85d24-bfd4-4a54-a0fd-af072680fd09",
"meta": {
"lastUpdated": "2025-11-05T05:09:13.478+00:00"
},
"type": "searchset",
"total": 5,
"link": [
{
"relation": "self",
"url": "http://r4.openfhir.org:8080/fhir/Patient/224/$everything?_count=50&_type=Observation"
}
],
"entry": [
{
"fullUrl": "http://r4.openfhir.org:8080/fhir/Patient/224",
"resource": {
"resourceType": "Patient",
"id": "224",
"meta": {
"versionId": "1",
"lastUpdated": "2025-09-12T15:30:12.065+00:00"
},
"identifier": [
{
"system": "www.mypatientidentifier.com/ids",
"value": "andres.kury"
}
],
"name": [
{
"family": "kury",
"given": [
"andres"
]
}
]
},
"search": {
"mode": "match"
}
},
{
"fullUrl": "http://r4.openfhir.org:8080/fhir/Observation/219",
"resource": {
"resourceType": "Observation",
"id": "219",
"meta": {
"versionId": "1",
"lastUpdated": "2025-09-12T15:30:12.065+00:00"
},
"status": "final",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "8480-6",
"display": "Systolic blood pressure"
}
]
},
"subject": {
"reference": "Patient/224"
},
"effectiveDateTime": "2025-09-09T10:30:00-06:00",
"valueQuantity": {
"value": 120,
"unit": "mmHg"
}
},
"search": {
"mode": "match"
}
},
{
"fullUrl": "http://r4.openfhir.org:8080/fhir/Observation/220",
"resource": {
"resourceType": "Observation",
"id": "220",
"meta": {
"versionId": "1",
"lastUpdated": "2025-09-12T15:30:12.065+00:00"
},
"status": "final",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "8462-4",
"display": "Diastolic blood pressure"
}
]
},
"subject": {
"reference": "Patient/224"
},
"effectiveDateTime": "2025-09-09T10:30:00-06:00",
"valueQuantity": {
"value": 80,
"unit": "mmHg"
}
},
"search": {
"mode": "match"
}
},
{
"fullUrl": "http://r4.openfhir.org:8080/fhir/Observation/221",
"resource": {
"resourceType": "Observation",
"id": "221",
"meta": {
"versionId": "1",
"lastUpdated": "2025-09-12T15:30:12.065+00:00"
},
"status": "final",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "8867-4",
"display": "Heart rate"
}
]
},
"subject": {
"reference": "Patient/224"
},
"effectiveDateTime": "2025-09-09T10:30:00-06:00",
"valueQuantity": {
"value": 72,
"unit": "/min"
}
},
"search": {
"mode": "match"
}
},
{
"fullUrl": "http://r4.openfhir.org:8080/fhir/Observation/222",
"resource": {
"resourceType": "Observation",
"id": "222",
"meta": {
"versionId": "1",
"lastUpdated": "2025-09-12T15:30:12.065+00:00"
},
"status": "final",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "9279-1",
"display": "Respiratory rate"
}
]
},
"subject": {
"reference": "Patient/224"
},
"effectiveDateTime": "2025-09-09T10:30:00-06:00",
"valueQuantity": {
"value": 16,
"unit": "/min"
}
},
"search": {
"mode": "match"
}
}
]
}
The execution took 416 milliseconds. I then went ahead and did it with GraphQL to see how it would go — remember, GraphQL lets you simply request the data you want, so the query ended up like this:
query {
patient: Patient(id: "224") { ...Pat }
observations: ObservationList(subject: "Patient/224", _count: 50) { ...Obs }
}
fragment Pat on Patient {
id birthDate name { given family }
}
fragment Obs on Observation {
id status effectiveDateTime
code { text coding { system code display } }
valueQuantity { value unit }
valueCodeableConcept { text coding { system code display } }
valueString
}
The server's response came back in 111 milliseconds. The consistency in speed and efficiency is exactly the same: 73% more efficient (3.75x faster). At this point I was already convinced I'd switch to pure GraphQL for consuming FHIR resources.
But well, so far so good — what if we ask for something a bit "heavier," like a patient summary representation? Let's see how that goes. The "general" query looks like this:
GET https://hapi.fhir.org/baseR4/Patient/7011747/$summary
The server's response was quite large, but it took 1.37 seconds — now a more complex query, where the returned JSON contained 2,619 lines. Now let's go with GraphQL. Clarification: FHIR doesn't specify how to obtain a $summary operation, so we're going to emulate it like this:
query SummaryQuery {
patient: Patient(id: "7011747") {
id
name { given family }
gender
birthDate
address { city country }
telecom { system value }
}
problems: ConditionList(patient: "7011747", category: "problem-list-item", _count: 50) {
id
clinicalStatus { coding { ...CodingFields } }
verificationStatus { coding { ...CodingFields } }
code { ...Concept }
onsetDateTime
}
allergies: AllergyIntoleranceList(patient: "7011747", clinical_status: "active", _count: 50) {
id
code { ...Concept }
criticality
reaction { manifestation { ...Concept } severity }
}
medications: MedicationStatementList(patient: "7011747", _count: 50) {
id
status
medicationCodeableConcept { ...Concept }
effectiveDateTime
}
immunizations: ImmunizationList(patient: "7011747", _count: 50) {
id
status
vaccineCode { ...Concept }
occurrenceDateTime
}
# Labs: usa category "laboratory"
labs: ObservationList(patient: "7011747", category: "laboratory", _count: 50) {
id
status
code { ...Concept }
effectiveDateTime
valueQuantity { value unit }
valueCodeableConcept { ...Concept }
valueString
}
# Signos vitales: evita _profile; usa category "vital-signs"
vitals: ObservationList(patient: "7011747", category: "vital-signs", _count: 50) {
id
status
code { ...Concept }
effectiveDateTime
valueQuantity { value unit }
}
procedures: ProcedureList(patient: "7011747", _count: 50) {
id
status
code { ...Concept }
performedDateTime
}
}
fragment CodingFields on Coding {
system
code
display
}
fragment Concept on CodeableConcept {
text
coding { ...CodingFields }
}
The server returned the response in 269 milliseconds; in this case, the efficiency gain was 80% (5.09x faster). The response JSON contained only 321 lines.
Conclusions
- GraphQL on FHIR does deliver real efficiency: in these tests, GraphQL queries were consistently faster and returned smaller payloads than their REST equivalents.
- Less "noise," more control: GraphQL lets you request only the fields you need, reduce over/under-fetching, and traverse references in a single query (e.g.,
Patient+Observation), which simplifies consumption from the frontend and intermediate services. - Spec status: the
$graphqloperation has been available since FHIR R4 and remains Trial Use.
GraphQL doesn't replace REST in FHIR, but it's an excellent accelerator for clinical experiences that need field-level precision and aggregation of multiple resources with low latency. Adopting it as an optional layer gives you performance gains and better client ergonomics without compromising the underlying interoperability.


