fhir-software
SKILL.md
FHIR Software Development Skill
Expert guidance for building robust FHIR (Fast Healthcare Interoperability Resources) software systems with comprehensive package management, spec knowledge, and development workflows.
Core Architecture
1. Package/Specification Management
Local FHIR Package Cache:
- Use
@fhir/package-loaderor equivalent for TypeScript/Node.js environments - For Python:
fhir-package-loaderor custom implementation usingrequests+json - Cache strategy:
~/.fhir/packages/with version-specific directories - Support packages:
hl7.fhir.r4.core,hl7.fhir.r5.core, Implementation Guides
Package Resolution Pattern:
// Load and cache FHIR packages
async function loadFhirPackage(packageId: string, version?: string) {
const cacheDir = path.join(os.homedir(), '.fhir', 'packages', packageId, version || 'current');
if (await fs.pathExists(cacheDir)) return loadFromCache(cacheDir);
const packageData = await downloadPackage(packageId, version);
await cachePackage(cacheDir, packageData);
return packageData;
}
Document Index Structure: Build searchable index from package contents:
- StructureDefinitions (resources, profiles, extensions)
- SearchParameters (for API implementation)
- ValueSets and CodeSystems (terminology)
- OperationDefinitions (custom operations)
- CapabilityStatements (server capabilities)
- Example instances
2. Development Workflows
FHIR Resource Modeling
# Use Pydantic for FHIR resource modeling in Python
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List, Literal
from enum import Enum
import re
class PatientGender(str, Enum):
MALE = "male"
FEMALE = "female"
OTHER = "other"
UNKNOWN = "unknown"
class Patient(BaseModel):
resourceType: Literal["Patient"] = "Patient"
id: Optional[str] = None
active: Optional[bool] = None
name: Optional[List[dict]] = None
gender: Optional[PatientGender] = None
birthDate: Optional[str] = None
@field_validator('birthDate')
@classmethod
def validate_birthdate(cls, v):
if v and not re.match(r'^\d{4}-\d{2}-\d{2}$', v):
raise ValueError('Invalid date format, must be YYYY-MM-DD')
return v
class Config:
extra = "allow" # Allow additional FHIR elements
FHIR Server Implementation Patterns
FastAPI + Pydantic (Python):
from fastapi import FastAPI, HTTPException
from fhir.resources.patient import Patient
app = FastAPI()
@app.post("/Patient", response_model=Patient)
async def create_patient(patient: Patient):
# Validate against FHIR spec
patient.validate()
# Store in database
saved_patient = await db.save_patient(patient)
return saved_patient
@app.get("/Patient/{patient_id}")
async def get_patient(patient_id: str):
patient = await db.get_patient(patient_id)
if not patient:
raise HTTPException(404, "Patient not found")
return patient
Express + FHIR TypeScript (Node.js):
import express from 'express';
import { Patient, Bundle } from 'fhir/r4';
const app = express();
app.post('/Patient', (req, res) => {
const patient: Patient = req.body;
// Validate resource type and required fields
if (patient.resourceType !== 'Patient') {
return res.status(400).json({
resourceType: 'OperationOutcome',
issue: [{
severity: 'error',
code: 'invalid',
details: { text: 'Invalid resource type' }
}]
});
}
// Process and store
const savedPatient = db.savePatient(patient);
res.status(201).json(savedPatient);
});
3. FHIR Validation and Quality
Profile Validation
from fhir.resources.core.fhirabstractmodel import FHIRAbstractModel
from fhir.resources import get_fhir_model_class
import json
def validate_against_profile(resource_data: dict, profile_url: str) -> bool:
"""Validate FHIR resource against specific profile"""
try:
# Load profile from package cache
profile = load_structure_definition(profile_url)
# Validate using fhir.resources - dynamically get the resource class
resource_type = resource_data.get('resourceType')
resource_class = get_fhir_model_class(resource_type)
resource = resource_class(**resource_data)
# Additional profile-specific validation
return validate_profile_constraints(resource, profile)
except Exception as e:
print(f"Validation error: {e}")
return False
Terminology Validation
def validate_coding(coding: dict, value_set_url: str) -> bool:
"""Validate coding against ValueSet"""
value_set = load_value_set(value_set_url)
# Check if code exists in ValueSet expansion
for concept in value_set.get('expansion', {}).get('contains', []):
if (concept.get('code') == coding.get('code') and
concept.get('system') == coding.get('system')):
return True
return False
4. FHIR Search Implementation
Search Parameter Processing
from typing import Dict, Any
import re
class FhirSearchProcessor:
def __init__(self):
self.search_params = load_search_parameters()
def parse_search_query(self, resource_type: str, params: Dict[str, str]) -> Dict[str, Any]:
"""Parse FHIR search parameters into database query"""
query = {}
for param_name, param_value in params.items():
search_param = self.get_search_parameter(resource_type, param_name)
if not search_param:
continue
# Handle different search parameter types
if search_param['type'] == 'string':
query[param_name] = self.parse_string_search(param_value)
elif search_param['type'] == 'token':
query[param_name] = self.parse_token_search(param_value)
elif search_param['type'] == 'date':
query[param_name] = self.parse_date_search(param_value)
elif search_param['type'] == 'reference':
query[param_name] = self.parse_reference_search(param_value)
return query
def parse_token_search(self, value: str) -> Dict[str, str]:
"""Parse token search: [system]|[code]"""
if '|' in value:
system, code = value.split('|', 1)
result = {}
if system:
result['system'] = system
if code:
result['code'] = code
return result
return {'code': value} if value else {}
5. SMART on FHIR Integration
OAuth 2.0 / SMART App Launch
interface SmartConfig {
authorizeUrl: string;
tokenUrl: string;
clientId: string;
redirectUri: string;
scopes: string[];
}
class SmartClient {
constructor(private config: SmartConfig) {}
async authorize(): Promise<string> {
const state = generateRandomState();
const authUrl = new URL(this.config.authorizeUrl);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', this.config.clientId);
authUrl.searchParams.set('redirect_uri', this.config.redirectUri);
authUrl.searchParams.set('scope', this.config.scopes.join(' '));
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('aud', getFhirBaseUrl());
return authUrl.toString();
}
async exchangeCodeForToken(code: string): Promise<TokenResponse> {
const response = await fetch(this.config.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.config.clientId,
code,
redirect_uri: this.config.redirectUri
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.statusText}`);
}
return response.json();
}
}
Implementation Guides and Extensions
Custom Profile Development
{
"resourceType": "StructureDefinition",
"id": "my-patient-profile",
"url": "http://example.org/fhir/StructureDefinition/MyPatient",
"name": "MyPatientProfile",
"status": "draft",
"kind": "resource",
"abstract": false,
"type": "Patient",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
"derivation": "constraint",
"differential": {
"element": [
{
"id": "Patient.identifier",
"path": "Patient.identifier",
"min": 1,
"mustSupport": true
},
{
"id": "Patient.name",
"path": "Patient.name",
"min": 1,
"max": "1"
}
]
}
}
Testing and Validation Tools
Unit Testing FHIR Resources
import pytest
from fhir.resources.patient import Patient
def test_patient_creation():
patient_data = {
"resourceType": "Patient",
"id": "example",
"active": True,
"name": [{
"family": "Doe",
"given": ["John"]
}],
"gender": "male"
}
patient = Patient(**patient_data)
assert patient.resourceType == "Patient"
assert patient.gender == "male"
assert len(patient.name) == 1
assert patient.name[0].family == "Doe"
FHIR Server Testing
import httpx
import pytest
@pytest.fixture
async def fhir_client():
async with httpx.AsyncClient(base_url="http://localhost:8000") as client:
yield client
async def test_patient_crud(fhir_client):
# Create patient
patient_data = {"resourceType": "Patient", "active": True}
create_response = await fhir_client.post("/Patient", json=patient_data)
assert create_response.status_code == 201
patient_id = create_response.json()["id"]
# Read patient
read_response = await fhir_client.get(f"/Patient/{patient_id}")
assert read_response.status_code == 200
# Update patient
updated_data = read_response.json()
updated_data["active"] = False
update_response = await fhir_client.put(f"/Patient/{patient_id}", json=updated_data)
assert update_response.status_code == 200
# Delete patient
delete_response = await fhir_client.delete(f"/Patient/{patient_id}")
assert delete_response.status_code == 204
Common Patterns and Best Practices
Resource References and Includes
def resolve_references(resource: dict, include_params: List[str]) -> dict:
"""Resolve _include parameters for FHIR search"""
included_resources = []
for include_param in include_params:
source_type, search_param = include_param.split(':', 1)
if resource.get('resourceType') == source_type:
ref_values = extract_reference_values(resource, search_param)
for ref_value in ref_values:
referenced_resource = load_resource_by_reference(ref_value)
if referenced_resource:
included_resources.append(referenced_resource)
return {
'resourceType': 'Bundle',
'type': 'searchset',
'entry': [{'resource': resource}] + [{'resource': r} for r in included_resources]
}
Batch and Transaction Processing
async def process_bundle(bundle: dict) -> dict:
"""Process FHIR Bundle with batch or transaction semantics"""
response_entries = []
transaction_mode = bundle.get('type') == 'transaction'
try:
if transaction_mode:
await db.begin_transaction()
for entry in bundle.get('entry', []):
request = entry.get('request', {})
resource = entry.get('resource')
response_entry = await process_bundle_entry(request, resource)
response_entries.append(response_entry)
if transaction_mode:
await db.commit_transaction()
except Exception as e:
if transaction_mode:
await db.rollback_transaction()
raise
return {
'resourceType': 'Bundle',
'type': 'transaction-response' if transaction_mode else 'batch-response',
'entry': response_entries
}
Error Handling and OperationOutcome
def create_operation_outcome(severity: str, code: str, details: str) -> dict:
"""Create FHIR OperationOutcome for error reporting"""
return {
'resourceType': 'OperationOutcome',
'issue': [{
'severity': severity,
'code': code,
'details': {'text': details}
}]
}
# Usage in API endpoints
try:
result = validate_fhir_resource(resource_data)
except ValidationError as e:
return create_operation_outcome('error', 'invalid', str(e)), 400
Quick Reference Commands
Package Management
# Install FHIR packages
npm install @types/fhir @fhir/package-loader
pip install fhir.resources fhir-package-loader
# Load core FHIR packages
fhir-package-loader install hl7.fhir.r4.core 4.0.1
fhir-package-loader install hl7.fhir.us.core 5.0.1
Validation Tools
# FHIR Validator (Java)
java -jar validator_cli.jar resource.json -version 4.0.1
# HAPI FHIR Validator
curl -X POST "http://localhost:8080/fhir/$validate" \
-H "Content-Type: application/fhir+json" \
-d @patient.json
Development Server Setup
# Python FastAPI FHIR Server
uvicorn main:app --reload --port 8000
# Node.js Express FHIR Server
npm start
Integration Points
- EHR Systems: Epic, Cerner, AllScripts FHIR APIs
- Cloud Platforms: AWS HealthLake, Azure FHIR, Google Healthcare API
- Terminology Services: UMLS, SNOMED CT, LOINC
- Security: OAuth 2.0, JWT, SMART on FHIR scopes
- Interoperability: HL7 v2 to FHIR conversion, CDA to FHIR
For detailed implementation guidance, reference the FHIR specification at https://hl7.org/fhir/ and implementation guides at https://fhir.org/guides/registry/
6. FHIR Shorthand (FSH) & IG Authoring
Toolchain
| Tool | Role | Install |
|---|---|---|
| SUSHI | Compiles .fsh files → FHIR JSON |
npm install -g fsh-sushi |
| GoFSH | Converts FHIR JSON → FSH source | npm install -g gofsh |
| IG Publisher | Renders IG website from SUSHI output | Downloaded via _updatePublisher.sh |
SUSHI Commands
sushi init # Scaffold new IG project
sushi build # Compile FSH → FHIR JSON (from project root)
sushi build . --snapshot # Include full StructureDefinition snapshot
sushi build . --log-level debug # Verbose output
sushi build . --preprocessed # Debug: dump resolved aliases/rulesets
sushi update-dependencies # Update deps to latest
Output: fsh-generated/resources/{ResourceType}-{id}.json. SUSHI clears this folder on every build — never edit it manually.
GoFSH Commands
gofsh ./fhir-resources # Convert JSON artifacts to FSH
gofsh ./definitions -d hl7.fhir.us.core@6.1.0 # With extra dependencies
gofsh ./definitions --style group-by-profile # Organize output by profile
gofsh ./definitions --indent # Use indented rule style
gofsh ./definitions --fshing-trip # Validate round-trip accuracy
Project Structure
my-ig/
├── sushi-config.yaml # Required: IG configuration
├── ig.ini # Required for IG Publisher
├── _genonce.sh / _genonce.bat # Run IG Publisher
├── _updatePublisher.sh / .bat # Download latest IG Publisher jar
├── input/
│ ├── fsh/ # All FSH source files
│ │ ├── aliases.fsh # Alias: $LNC = http://loinc.org
│ │ ├── profiles.fsh
│ │ ├── extensions.fsh
│ │ ├── valuesets.fsh
│ │ ├── codesystems.fsh
│ │ ├── instances.fsh
│ │ └── rulesets.fsh
│ ├── pagecontent/ # IG narrative (Markdown)
│ │ ├── index.md # Home page
│ │ ├── 1_background.md # Numbered = TOC order
│ │ ├── {resource-id}-intro.md # Content before artifact
│ │ └── {resource-id}-notes.md # Content after artifact
│ ├── images/ # Images, PDFs, spreadsheets
│ └── ignoreWarnings.txt
└── fsh-generated/ # SUSHI output (auto-generated, do not edit)
sushi-config.yaml (Minimal Required Fields)
id: hl7.fhir.us.example
canonical: http://hl7.org/fhir/us/example
name: ExampleIG
title: "Example Implementation Guide"
status: draft # draft | active | retired | unknown
version: 0.1.0
fhirVersion: 4.0.1
copyrightYear: 2024+
releaseLabel: ci-build
publisher:
name: My Organization
url: http://example.org
dependencies:
hl7.fhir.us.core: 6.1.0
menu:
Home: index.html
Artifacts: artifacts.html
parameters:
show-inherited-invariants: false
FSH Entity Types
Profile
Profile: MyPatientProfile
Parent: Patient
Id: my-patient-profile
Title: "My Patient Profile"
Description: "Constrained Patient for our IG"
* identifier 1..* MS
* name 1..* MS
* birthDate MS
* gender 1..1 MS
* gender from http://hl7.org/fhir/ValueSet/administrative-gender (required)
Extension (Simple)
Extension: PatientReligion
Id: patient-religion
Title: "Patient Religion"
Context: Patient
* value[x] only CodeableConcept
* value[x] from ReligionValueSet (extensible)
Extension (Complex — sub-extensions)
Extension: USCoreEthnicityExtension
Id: us-core-ethnicity
Context: Patient, RelatedPerson, Practitioner
* extension contains
ombCategory 0..1 MS and
detailed 0..* and
text 1..1 MS
* extension[ombCategory].value[x] only Coding
* extension[ombCategory].value[x] from OmbEthnicityCategories (required)
* extension[text].value[x] only string
Instance
Instance: JaneDoe
InstanceOf: MyPatientProfile
Title: "Jane Doe"
Usage: #example // #example | #definition | #inline
* name[0].family = "Doe"
* name[0].given[0] = "Jane"
* birthDate = 1970-01-01
* gender = #female
ValueSet
ValueSet: MyConditionStatusVS
Id: my-condition-status
Title: "Condition Status Codes"
* include codes from system $SCT where concept is-a #404684003
* $V3#active "Active"
* exclude $SCT#74964007 "Other"
CodeSystem
CodeSystem: MyCustomCodes
Id: my-custom-codes
* #pending "Pending" "Awaiting review"
* #approved "Approved" "Formally approved"
// Hierarchical:
* #body "Body"
* #head "Head"
Invariant + Obeys
Invariant: my-inv-1
Description: "Value must be present for vital-signs category"
Severity: #error
Expression: "category.coding.code = 'vital-signs' implies value.exists()"
// In a Profile:
* obeys my-inv-1
RuleSet (Reusable / Parameterized)
RuleSet: PublicationMetadata
* ^status = #active
* ^experimental = false
* ^publisher = "My Org"
// Parameterized
RuleSet: SetContext(contextPath)
* ^context[+].type = #element
* ^context[=].expression = "{contextPath}"
// Usage:
* insert PublicationMetadata
* insert SetContext(Patient)
FSH Rules Quick Reference
| Rule | Syntax | Example |
|---|---|---|
| Cardinality | * elem min..max |
* name 1..* MS |
| Must Support | * elem MS |
* identifier MS |
| Type constraint | * elem only Type |
* value[x] only Quantity |
| Binding | * elem from VS (strength) |
* code from MyVS (required) |
| Fixed value | * elem = value |
* status = #final |
| Fixed coding | * elem = $SYS#code "display" |
* code = $LNC#29463-7 |
| Quantity | * elem = n 'unit' |
* value = 70 'kg' |
| Slice (element) | * arr contains name card |
* component contains systolic 1..1 |
| Slice (extension) | * extension contains Ext named n card |
* extension contains $Race named race 0..1 |
| Obeys | * obeys inv-id |
* obeys us-core-6 |
| Caret (metadata) | * ^property = val |
* ^experimental = false |
| Insert RuleSet | * insert RSName |
* insert PublicationMetadata |
Path Grammar Quick Reference
status // Simple element
name.family // Nested
valueQuantity // Choice [x] resolved
name[0] // Array index (zero-based)
name[+] // Soft index: next slot
name[=] // Soft index: same slot
component[respirationScore] // Slice by name
extension[race] // Extension slice
performer[Practitioner] // Reference target
^experimental // StructureDefinition metadata
code ^short // ElementDefinition property
Coding / Quantity Syntax
// Alias declaration (top of file)
Alias: $LNC = http://loinc.org
Alias: $SCT = http://snomed.info/sct
// Code (no system)
#active
// Coding
http://loinc.org#29463-7 "Body Weight"
$LNC#29463-7 "Body Weight"
// Quantity (UCUM)
70.5 'kg' "kg"
120 'mm[Hg]' "mmHg"
Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Profile/Extension/RuleSet names | PascalCase |
MyPatientProfile |
| Item IDs | kebab-case, max 64 chars |
my-patient-profile |
| Slice names | lowerCamelCase |
respirationScore |
| Alias names | $PrefixedName |
$LNC, $SCT |
Key Rules
- Declare slices before constraining them —
containsrule must precede slice-specific rules - Declare extensions before constraining sub-elements — same ordering requirement
fsh-generated/is owned by SUSHI — never edit it; it is deleted and regenerated on each build- Caret (
^) rules are forbidden in Instances — use them only in Profiles, Extensions, ValueSets, CodeSystems
IG Authoring Workflow
# New IG from scratch
mkdir my-ig && cd my-ig
sushi init # Interactive scaffold
# Edit sushi-config.yaml and write .fsh files
sushi build # Compile
./_genonce.sh # Run IG Publisher
# Migrate existing FHIR JSON to FSH
gofsh ./existing-resources -d hl7.fhir.us.core@6.1.0 \
--style group-by-profile --indent --fshing-trip
# Debug a failing build
sushi build --log-level debug
sushi build --preprocessed # Inspect resolved aliases/rulesets
Reference Files
- Full spec: https://build.fhir.org/ig/HL7/fhir-shorthand/reference.html
- SUSHI docs: https://fshschool.org/docs/sushi/
- GoFSH docs: https://fshschool.org/docs/gofsh/
- See also:
references/fsh_ig_authoring.mdfor the complete reference
Weekly Installs
6
Repository
topologyhealth/…hirskillGitHub Stars
40
First Seen
13 days ago
Security Audits
Installed on
gemini-cli5
opencode4
claude-code4
github-copilot4
amp4
cline4