FHIR has become the primary standard for exchanging healthcare data. To get started with FHIR, developers need to read the FHIR specification and implementation guides (IGs, such as US Core, MCODE, etc.), and then implement them in their programming language. FHIR SDKs simplify this process by offering well-documented tools that work naturally within a developer’s programming language.
In this post, we'll explain what a FHIR SDK is, why creating your own SDK can be better than using a universal one, and how Type Schema, an open-source tool, helps you generate a custom SDK tailored to your specific FHIR resources.
‍
To build a useful FHIR SDK, it's important to understand its core responsibilities – how it communicates with the FHIR server and how it handles structured data. Let's break down what a typical SDK includes:
On the operation side, the SDK provides methods to communicate with a FHIR Server. It includes functions for building URLs, marshaling data, handling pagination, logging in, authentication, and more. For example:
const patient = await fhirClient.read('Patient', '123');
const observations = await fhirClient.search('Observation', {
patient: 'Patient/123',
code: 'http://loinc.org|8867-4',
date: 'gt2022-01-01'
});
const result = await fhirClient.create(newPatientResource);
Implementation note: These parts work closely with your programming language's tools, like HTTP libraries and how it handles tasks that take time. Ideally, the SDK should be native to your project stack.
On the resource side, an SDK provides types or classes that match FHIR resource definitions, including their fields and constraints (over 150 in the basic FHIR spec). They should be native to use in your language:
const patient = new Patient({
name: [
new HumanName({
family: "Smith",
given: ["John"]
})
],
birthDate: "1970-01-01"
});
Implementation note: Given the number of resources, code is usually generated automatically. Resource types may also integrate with the operation part of the SDK (e.g., active record pattern).
‍
A universal FHIR SDK (one that works with all FHIR features and versions) presents several challenges:
At the same time, building a FHIR SDK from scratch is hard for someone new to FHIR (like trying to build an ORM after just learning SQL). The biggest challenges are:
Most challenges belong to resources/types layer of the SDK. To simplify FHIR SDK development, we created Type Schema.
‍
Type Schema is a community-driven tool that makes FHIR SDK development easier.
FHIR Schema Specification is a JSON format that represents FHIR data in a way that's easy to learn and generate code from. Here's why:
FHIR Schema Tools are open-source utilities (MIT licensed) that create Type Schemas for FHIR, IGs and your custom resources.
The main tool is Type Schema, which takes packages and custom resource definitions and generates Type Schemas ready for code generation.
You can also read more here:
‍
Let's look at an example. We'll start with the Patient resource from hl7.fhir.r4.core
in TypeScript and show how to generate this code from Type Schema step by step.
Full code examples are available here:
‍
Since hl7.fhir.r4.core
includes many resources and types, placing them in one file isn’t practical.Instead, we import them:
import { Address } from './Address';
import { Attachment } from './Attachment';
// ...
You can generate imports with:
const deps = schema.dependencies
// other types will be inlined or defined in this file
.filter((dep) => ['complex-type', 'resource'].includes(dep.kind))
.sort((a, b) => a.name.localeCompare(b.name))
.map((dep) => `import { ${this.uppercaseFirstLetter(dep.name)} } from './${dep.name}'`)
.join('\n');
All files can be generated in the same way as for the Patient resource.
‍
FHIR Resources have complex nested structures. Since many languages don’t support nested type definitions, we generate local types:
export interface PatientLink extends BackboneElement {
other?: Reference<'Patient' | 'RelatedPerson'>;
type?: 'replaced-by' | 'replaces' | 'refer' | 'seealso';
}
export interface PatientCommunication extends BackboneElement {
language?: CodeableConcept;
preferred?: boolean;
}
export interface PatientContact extends BackboneElement {
address?: Address;
gender?: 'male' | 'female' | 'other' | 'unknown';
name?: HumanName;
organization?: Reference<'Organization'>;
period?: Period;
relationship?: CodeableConcept[];
telecom?: ContactPoint[];
}
You can generate this from the .nested
field, with all dependencies already imported:
for (const subtype of schema.nested) {
this.generateType(subtype);
}
where generateType
is a function that receives a type schema and makes a type definition. Details are in the next section.
‍
What do we have at this step?
Let's look at a few cases (skipping extensions and most fields):
export interface Patient extends DomainResource {
active?: boolean;
link?: PatientLink[];
gender?: 'male' | 'female' | 'other' | 'unknown';
multipleBirthBoolean?: boolean;
multipleBirthInteger?: number;
// ...
}
The first line of type definition is simple: we take the type name from .identifier.name
and base type from .base.name
.
export interface Patient extends DomainResource {
{
"identifier": { "kind": "resource", "package": "hl7.fhir.r4.core", "version": "4.0.1",
"name": "Patient", "url": "http://hl7.org/fhir/StructureDefinition/Patient" },
"base": { "kind": "resource", "package": "hl7.fhir.r4.core", "version": "4.0.1",
"name": "DomainResource", "url": "http://hl7.org/fhir/StructureDefinition/DomainResource" }
}
Fields are generated by analyzing .fields, and mapped based on type:
const typeMap = { boolean: 'boolean'... }
[]
to the end of the name. active?: boolean;
link?: PatientLink[];
{
"active": {
"type": { "kind": "primitive-type", "package": "hl7.fhir.r4.core", "version": "4.0.1",
"name": "boolean", "url": "http://hl7.org/fhir/StructureDefinition/boolean" },
"array": false,
"required": false, "excluded": false
},
"link": {
"type": { "kind": "nested", "package": "hl7.fhir.r4.core", "version": "4.0.1",
"name": "link", "url": "http://hl7.org/fhir/StructureDefinition/Patient#link" },
"array": true,
"required": false, "excluded": false
}
}
For fields with ValueSet binding (lists of allowed values), Type Schema provides possible values so we can add them directly to our type:
gender?: 'male' | 'female' | 'other' | 'unknown';
{
"gender": {
"type": { "kind": "primitive-type", "package": "hl7.fhir.r4.core", "version": "4.0.1",
"name": "code", "url": "http://hl7.org/fhir/StructureDefinition/code" },
"array": false,
"required": false, "excluded": false,
"enum": [ "male", "female", "other", "unknown" ]
}
}
For choice types (fields that can take on different types), this example uses a simple approach by creating separate fields without additional validation. For other options see: Choice Type Representation.
multipleBirthBoolean?: boolean;
multipleBirthInteger?: number;
{
"multipleBirth": {
"choices": [ "multipleBirthBoolean", "multipleBirthInteger" ],
"array": false,
"required": false, "excluded": false
},
"multipleBirthBoolean": {
"choiceOf": "multipleBirth",
"type": { "kind": "primitive-type", "package": "hl7.fhir.r4.core", "version": "4.0.1",
"name": "boolean", "url": "http://hl7.org/fhir/StructureDefinition/boolean" },
"array": false,
"required": false, "excluded": false
},
"multipleBirthInteger": {
"choiceOf": "multipleBirth",
"type": { "kind": "primitive-type", "package": "hl7.fhir.r4.core", "version": "4.0.1",
"name": "integer", "url": "http://hl7.org/fhir/StructureDefinition/integer" },
"array": false,
"required": false, "excluded": false
}
}
This is just a glimpse into generating FHIR SDK code with Type Schema, but as you can see, it’s straightforward if you know what you’re aiming to produce.
‍
To generate SDK types:
1. Install code generator:
npm install -g @fhirschema/codegen
2. Generate types for
• TypeScript:
npx @fhirschema/codegen generate -g typescript -p 'hl7.fhir.r4.core@4.0.1' -o out
• Python:
npx @fhirschema/codegen generate -g python -p 'hl7.fhir.r4.core@4.0.1' -o out
• C#:
npx @fhirschema/codegen generate -g csharp -p 'hl7.fhir.r4.core@4.0.1' -o out
For more detail please check: fhir-schema-codegen.
‍
In this post, we introduced Type Schema as a tool to simplify FHIR SDK development. By providing a standardized format for FHIR data entities, developers can generate custom SDKs tailored to their needs without starting from scratch.
‍
Type Schema is still in early development. Upcoming improvements include:
‍
Help us to make Type Schema better:
Get in touch with us today!