Skip to content

GraphQL API

OctoFHIR provides a GraphQL API following the GraphQL for FHIR specification. It offers a flexible alternative to the REST API for querying and mutating FHIR resources.

  1. Enable GraphQL in configuration

    octofhir.toml
    [graphql]
    enabled = true
    introspection = true # Disable in production
  2. Get an access token

    Terminal window
    TOKEN=$(curl -s -X POST http://localhost:8888/oauth/token \
    -d "grant_type=password&username=admin&password=admin123" \
    | jq -r '.access_token')
  3. Query a resource

    Terminal window
    curl http://localhost:8888/\$graphql \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"query": "{ PatientList { id name { given family } } }"}'

EndpointMethodDescription
/$graphqlPOSTSystem-level GraphQL endpoint
/$graphqlGETQuery via query URL parameter
/{Type}/{id}/$graphqlPOSTInstance-level (resource in context)

Query a resource by ID:

query {
Patient(_id: "patient-123") {
id
meta {
versionId
lastUpdated
}
name {
given
family
}
birthDate
gender
}
}

Search with FHIR search parameters:

query {
PatientList(
name: "Smith"
gender: "female"
_count: 10
_sort: "-birthDate"
) {
id
name {
given
family
}
birthDate
}
}

For large result sets, use connection-style pagination:

query {
PatientConnection(first: 10, after: "cursor123") {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
id
name { family }
}
}
totalCount
}
}

Resolve references automatically:

query {
ObservationList(code: "29463-7") {
id
code {
coding {
system
code
display
}
}
valueQuantity {
value
unit
}
subject {
reference
resource {
... on Patient {
id
name { given family }
}
}
}
performer {
resource {
... on Practitioner {
id
name { given family }
}
}
}
}
}

Find resources that reference a given resource:

query {
Patient(_id: "patient-123") {
id
name { family }
# All observations for this patient
ObservationList(_reference: "subject") {
id
code { text }
effectiveDateTime
}
# All conditions for this patient
ConditionList(_reference: "subject") {
id
code { text }
clinicalStatus { coding { code } }
}
}
}

mutation {
PatientCreate(res: {
resourceType: "Patient"
name: [{
given: ["John"]
family: "Doe"
}]
gender: "male"
birthDate: "1990-01-15"
}) {
id
meta {
versionId
lastUpdated
}
name { given family }
}
}
mutation {
PatientUpdate(
id: "patient-123"
res: {
resourceType: "Patient"
id: "patient-123"
name: [{
given: ["John", "William"]
family: "Doe"
}]
gender: "male"
birthDate: "1990-01-15"
}
) {
id
meta { versionId }
name { given family }
}
}
mutation {
PatientDelete(id: "patient-123") {
id
}
}

[graphql]
enabled = true
subscriptions = true
subscription {
# All resource changes
resourceChanged(resourceType: "Patient") {
eventType # CREATED, UPDATED, DELETED
resourceType
resourceId
resource {
... on Patient {
id
name { family }
}
}
}
}
subscription {
# Specific event types
resourceCreated(resourceType: "Observation") {
resourceId
resource {
... on Observation {
code { text }
valueQuantity { value unit }
}
}
}
}

GraphQL uses the same search parameters as the REST API:

ParameterDescription
_idResource ID
_lastUpdatedLast modification time
_countResults per page
_offsetSkip first N results
_sortSort order (prefix - for descending)
query {
# Patient search parameters
PatientList(
name: "John"
family: "Doe"
birthdate: "ge1990-01-01"
gender: "male"
identifier: "MRN|12345"
) { id }
# Observation search parameters
ObservationList(
code: "29463-7"
date: "ge2024-01-01"
patient: "Patient/123"
status: "final"
value_quantity: "gt100"
) { id }
}

Search modifiers work the same as REST:

query {
PatientList(
name_exact: "John Smith" # :exact modifier
name_contains: "smith" # :contains modifier
deceased_missing: false # :missing modifier
) { id name { text } }
}

[graphql]
# Enable GraphQL endpoints
enabled = true
# Maximum query depth (prevents deeply nested queries)
max_depth = 15
# Maximum query complexity score
max_complexity = 500
# Enable schema introspection (disable in production)
introspection = true
# Enable real-time subscriptions
subscriptions = false
# Allow batched queries
batching = false
# Maximum operations per batch
max_batch_size = 10

GraphQL errors follow the standard format:

{
"data": null,
"errors": [
{
"message": "Resource not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["Patient"],
"extensions": {
"code": "NOT_FOUND",
"resourceType": "Patient",
"resourceId": "invalid-id"
}
}
]
}
CodeDescription
NOT_FOUNDResource does not exist
UNAUTHORIZEDMissing or invalid token
FORBIDDENAccess denied by policy
VALIDATION_ERRORInvalid resource data
COMPLEXITY_EXCEEDEDQuery too complex
DEPTH_EXCEEDEDQuery too deeply nested

# Good - only request what you need
query {
PatientList { id name { family } }
}
# Avoid - requesting everything
query {
PatientList { id meta name ... }
}
fragment PatientBasics on Patient {
id
name { given family }
birthDate
gender
}
query {
patient1: Patient(_id: "p1") { ...PatientBasics }
patient2: Patient(_id: "p2") { ...PatientBasics }
}
query GetPatient($id: String!) {
Patient(_id: $id) {
id
name { given family }
}
}
{
"query": "query GetPatient($id: String!) { ... }",
"variables": { "id": "patient-123" }
}
query {
PatientConnection(first: 50) {
pageInfo { hasNextPage endCursor }
edges {
node { id name { family } }
}
}
}

FeatureRESTGraphQL
EndpointMultiple per resourceSingle /$graphql
Data fetchingFixed response shapeClient specifies fields
Related data_include/_revincludeNested queries
Batch requestsTransaction bundleMultiple queries
Real-timeSubscriptions (planned)Subscriptions
CachingHTTP cachingClient-side
  • Complex data requirements - Need data from multiple related resources
  • Mobile/bandwidth-sensitive - Request only needed fields
  • Interactive applications - Explore schema with introspection
  • Real-time updates - Use subscriptions
  • Simple CRUD - Standard create/read/update/delete
  • Bulk operations - Transactions, batch imports
  • HTTP caching - Leverage proxy caches
  • Tooling compatibility - Wide ecosystem support

query PatientWithObservations($patientId: String!) {
Patient(_id: $patientId) {
id
name { given family }
birthDate
ObservationList(_reference: "subject", _sort: "-date", _count: 10) {
id
code {
coding { system code display }
text
}
effectiveDateTime
valueQuantity { value unit }
status
}
}
}
mutation CreateVitalSign($patientId: String!, $value: Decimal!) {
ObservationCreate(res: {
resourceType: "Observation"
status: "final"
category: [{
coding: [{
system: "http://terminology.hl7.org/CodeSystem/observation-category"
code: "vital-signs"
}]
}]
code: {
coding: [{
system: "http://loinc.org"
code: "29463-7"
display: "Body Weight"
}]
}
subject: { reference: $patientId }
effectiveDateTime: "2024-01-15T10:30:00Z"
valueQuantity: {
value: $value
unit: "kg"
system: "http://unitsofmeasure.org"
code: "kg"
}
}) {
id
meta { versionId lastUpdated }
}
}
query ActiveDiabeticPatients {
ConditionList(
code: "http://snomed.info/sct|73211009"
clinical_status: "active"
_count: 100
) {
id
subject {
resource {
... on Patient {
id
name { given family }
birthDate
# Get their recent lab results
ObservationList(
_reference: "subject"
category: "laboratory"
_sort: "-date"
_count: 5
) {
code { text }
valueQuantity { value unit }
effectiveDateTime
}
}
}
}
}
}