Descargo de responsabilidad
Este artículo no pretende ser un tutorial de "GraphQL on FHIR", sino mostrar los beneficios de considerarlo para sus proyectos y ofrecer otro punto de vista.
Quise probar qué tal funciona y se comporta GraphQL con FHIR, así que me puse a "jugar" un rato principalmente en dos servidores:
- Servidor que utilizamos para el Curso de HL7 FHIR que impartió la Secretaría General del Sistema de la Integración Centroamericana (SG-SICA)
- Servidor Test dispuesto por HAPI FHIR
Pero antes de continuar y mostrar lo que descubrí, siento la obligación, por "cultura general", de presentar un poco de historia de cómo llegó GraphQL a HL7, específicamente a FHIR.
A partir del año 2019, HL7 decide incorporar en su especificación de FHIR R4 el soporte para el estándar GraphQL, creado inicialmente por Facebook (Meta) en el 2012. Nace como una respuesta para resolver los problemas de "over/under-fetching" que se presentaban en las apps móviles; la idea era unificar el acceso a datos dispersos.
¿Qué es GraphQL?
GraphQL es un lenguaje de consulta de código abierto para APIs que permite a los clientes solicitar exactamente los datos que necesitan, en lugar de recibir datos excesivos o incompletos. Es una alternativa más flexible y eficiente a las APIs RESTful que resuelve algunas de sus limitaciones, como la sobrecarga de datos. Opera con un sistema fuertemente tipado que describe los datos y permite a los clientes definir la estructura de la respuesta.
Para el año 2015, Facebook publica GraphQL como proyecto open source junto con una "reference implementation" en JavaScript y presenta el lenguaje oficialmente. Ya para el 2018, la Linux Foundation anuncia la GraphQL Foundation para custodiar la especificación y promover un ecosistema neutral, con apoyo de grandes actores del sector. Ese año también se consolida el SDL (Schema Definition Language) dentro de la especificación.
FHIR con GraphQL
En la versión de FHIR R4, HL7 incorpora la "operation" $graphql como alternativa al REST clásico, como Trial Use, con la operación estándar /$graphql y /{type}/{id}/$graphql. La operación formal $graphql está definida en la especificación como punto de entrada para ejecutarlo a nivel sistema o recurso.
Para el 2025, GraphQL en FHIR no es aún Normativo; sigue en Trial Use en la versión publicada (R5). El "build" continuo del estándar también lo muestra como Trial Use.
Pero aún en su estado no "Normativo", recomiendo utilizarlo ya que, al igual que en Facebook, evita el "over/under-fetching" y permite navegar referencias en una sola llamada — y es justo lo que les quiero mostrar.
Manos a la obra
Comencé a jugar con los servidores de menos a más, así que inicié con llamadas simples. Creo que los que llevamos ya un rato ensuciándonos las manos con FHIR tenemos nuestro "hola mundo": básicamente es hacer una consulta al recurso Patient. Así que ubiqué un recurso paciente en el servidor del curso de HL7 FHIR SG-SICA que utilizamos, con el id 35, y lo consumí:
GET http://r4.openfhir.org:8080/fhir/Patient/35
Esta consulta devolvió como respuesta el siguiente recurso:
{
"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"
}
]
}
]
}
La ejecución tardó 331 milisegundos, prácticamente nada, así que no me hice muchas expectativas con consultas tan pequeñas — pero igual quise probar con consumos mínimos.
Ahora bien, aquí inicia la magia de GraphQL: a mí no me interesa obtener toda la información que envía como respuesta el servidor FHIR, sino ciertos datos puntuales, y esto es justo lo que permite GraphQL:
query {
Patient(id: "35") {
id
name {
given
family
}
birthDate
gender
}
}
Aquí lo que estoy solicitando es que se retorne el id, nombre completo (nombres y apellidos), fecha de nacimiento y género. Al hacer la consulta, el servidor FHIR me devuelve lo siguiente:
{
"data": {
"Patient": {
"id": "Patient/35/_history/1",
"name": [
{
"given": [
"Andres Ernesto"
],
"family": "Kury Rivas"
}
],
"birthDate": "1996-07-09",
"gender": "male"
}
}
}
Como dirían algunos de mis amigos, "hermoso". A diferencia de la consulta GET, esta tardó 98 milisegundos, es decir, un 70% (3.38x más rápida) más eficiente que la consulta GET.
Bueno, caminé hacia algo un poco más complejo, así que busqué la manera de hacer una consulta para obtener la información del paciente y sus observaciones. Cuando hablamos de observaciones en FHIR nos referimos a:
- Signos vitales
- Datos de laboratorio
- Resultados de imágenes
- Hallazgos clínicos
- Mediciones de dispositivos
- Antecedentes sociales
Entre otros datos. Así que la consulta a ambos recursos en un "solo acto" quedó de la siguiente manera:
GET http://r4.openfhir.org:8080/fhir/Patient/224/$everything?_type=Observation&_count=50
Esta consulta devolvió como respuesta el siguiente recurso:
{
"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"
}
}
]
}
La ejecución tardó 416 milisegundos. Procedí a hacerlo con GraphQL a ver cómo me iba — recordemos que GraphQL permite pedir de manera simple qué datos deseo obtener, así que la consulta quedó de la siguiente manera:
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
}
La respuesta del servidor fue de 111 milisegundos. La consistencia de la velocidad y eficiencia es exactamente igual: un 73% más eficiente (3.75x más rápida). A este punto ya estaba convencido de que cambiaría la forma de consumir recursos FHIR a puro GraphQL.
Pero bueno, hasta aquí todo muy bien, pero ¿y si le pedimos algo un poco más "pesado", como una representación del resumen del paciente? Veamos cómo nos va. La consulta "general" queda de la siguiente manera:
GET https://hapi.fhir.org/baseR4/Patient/7011747/$summary
La respuesta del servidor fue bastante grande, pero tardó 1.37 segundos — ya una consulta más compleja, donde el JSON devuelto contenía 2.619 líneas. Ahora bien, vayamos con GraphQL. Aclaración: FHIR no especifica cómo obtener una operation $summary, así que la vamos a emular de la siguiente manera:
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 }
}
El servidor retornó la respuesta en 269 milisegundos; en este caso, la eficiencia fue de un 80% (5.09x más rápido). El JSON de respuesta contenía tan solo 321 líneas.
Conclusiones
- GraphQL en FHIR sí aporta eficiencia real: en las pruebas, las consultas con GraphQL fueron sistemáticamente más rápidas y con respuestas más pequeñas que sus equivalentes REST.
- Menos "ruido", más control: GraphQL permite pedir solo los campos necesarios, reducir el over/under-fetching y navegar referencias en una sola consulta (p. ej.,
Patient+Observation), lo que simplifica el consumo desde el frontend y los servicios intermedios. - Estado de la especificación: la operación
$graphqlestá disponible desde FHIR R4 y sigue en Trial Use.
GraphQL no reemplaza a REST en FHIR, pero es un acelerador excelente para experiencias clínicas que requieren exactitud en los campos y agregación de múltiples recursos con baja latencia. Adoptarlo como capa opcional te da ganancias de desempeño y ergonomía del cliente sin comprometer la interoperabilidad base.


