@simplefhir/cda
v0.1.1
Published
CDA and C-CDA XML clinical document parser and FHIR R4 transformer for SimpleFHIR
Readme
SimpleFHIR
Add FHIR R4 to a Node.js backend without running a separate FHIR server stack.
SimpleFHIR is a TypeScript-first FHIR R4 runtime for Node.js. It lets backend developers add FHIR routes, resource validation, search, persistence, versioning, CapabilityStatement generation, OperationOutcome errors, and interoperability features to an existing application with minimal configuration.
Quick Start
NestJS
npm install @simplefhir/nestjs// src/app.module.ts
import { Module } from '@nestjs/common';
import { FhirModule } from '@simplefhir/nestjs';
@Module({
imports: [
FhirModule.forRoot({
version: 'R4',
basePath: '/fhir',
}),
],
})
export class AppModule {}Start your application and SimpleFHIR exposes a FHIR endpoint alongside your existing API:
GET /fhir/metadata
GET /fhir/Patient
POST /fhir/Patient
GET /fhir/Patient/:id
PUT /fhir/Patient/:id
DELETE /fhir/Patient/:id
GET /fhir/Observation
POST /fhir/Observation
GET /fhir/Encounter
POST /fhir/EncounterTest it:
curl http://localhost:3000/fhir/metadata \
-H "Accept: application/fhir+json"Create a patient:
curl -X POST http://localhost:3000/fhir/Patient \
-H "Content-Type: application/fhir+json" \
-d '{
"resourceType": "Patient",
"name": [
{
"family": "Smith",
"given": ["Alice"]
}
],
"gender": "female",
"birthDate": "1990-04-12"
}'Search:
curl "http://localhost:3000/fhir/Patient?name=Smith&gender=female" \
-H "Accept: application/fhir+json"Why SimpleFHIR?
FHIR is a powerful interoperability standard, but adding a FHIR server surface to a Node.js application can require a large amount of infrastructure and specification-specific code.
Backend teams often end up choosing between:
- deploying a separate Java/JVM-based FHIR server;
- implementing FHIR REST behavior manually;
- building search, validation, versioning, Bundles, and
OperationOutcomehandling themselves; - coupling application code to a specific database or framework.
SimpleFHIR is designed to make that integration feel native to Node.js.
Existing Node.js Application
│
├── /api/users
├── /api/billing
├── /api/dashboard
│
└── SimpleFHIR
│
├── /fhir/metadata
├── /fhir/Patient
├── /fhir/Observation
└── /fhir/EncounterThe goal is simple:
Install a package, configure the resources you need, and add a real FHIR R4 API to your existing backend.
SimpleFHIR does not replace FHIR with a proprietary healthcare data model. The API surface remains FHIR.
What SimpleFHIR Handles
SimpleFHIR centralizes the FHIR-specific work that application developers should not have to rebuild for every project:
- FHIR R4 resource routing
- CRUD interactions
- FHIR search parsing
- SearchSet Bundles
CapabilityStatementgenerationOperationOutcomeerrors- resource validation
- resource versioning and history
- lifecycle hooks
- framework-independent authorization
- PostgreSQL persistence
- Prisma persistence
- SMART on FHIR helpers
- terminology operations
- HL7 v2 conversion
- C-CDA conversion
- Bulk Data export
- developer CLI tooling
Architecture
SimpleFHIR keeps FHIR logic separate from framework and database integrations.
┌───────────────────────────────────────────────┐
│ Existing Application │
│ NestJS / Express / Fastify │
└──────────────────────┬────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ Framework Adapter │
│ @simplefhir/nestjs │
│ @simplefhir/express │
│ @simplefhir/fastify │
└──────────────────────┬────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ @simplefhir/core │
│ │
│ Router Request Engine │
│ Resource Registry │
│ Search Parser + Search AST │
│ Validator │
│ Bundle Builder │
│ CapabilityStatement Builder │
│ OperationOutcome Builder │
│ Hooks + Authorization │
└──────────────────────┬────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ Storage Interface │
└──────────────────────┬────────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
@simplefhir/prisma @simplefhir/postgres
│ │
└──────────┬──────────┘
▼
PostgreSQLArchitectural rule
@simplefhir/core contains FHIR knowledge.
It should not contain NestJS, Express, Fastify, Prisma, or PostgreSQL-specific logic.
Framework and storage integrations are implemented as adapters around the core runtime.
Packages
| Package | Purpose |
|---|---|
| @simplefhir/core | Framework-independent FHIR R4 runtime |
| @simplefhir/nestjs | NestJS integration |
| @simplefhir/express | Express router and middleware |
| @simplefhir/fastify | Fastify plugin |
| @simplefhir/prisma | Prisma + PostgreSQL storage adapter |
| @simplefhir/postgres | Direct PostgreSQL storage adapter |
| @simplefhir/terminology | Terminology lookup, expansion, and validation |
| @simplefhir/smart | SMART on FHIR discovery and scope enforcement |
| @simplefhir/hl7v2 | HL7 v2 parsing and FHIR conversion |
| @simplefhir/cda | C-CDA parsing and FHIR conversion |
| @simplefhir/bulk-data | FHIR Bulk Data export |
| @simplefhir/cli | Validation, scaffolding, and inspection CLI |
Supported FHIR Resources
SimpleFHIR can be configured to expose only the resources your application needs.
Example:
FhirModule.forRoot({
version: 'R4',
resources: [
'Patient',
'Practitioner',
'Organization',
'Encounter',
'Observation',
'Condition',
'AllergyIntolerance',
'MedicationRequest',
'DiagnosticReport',
'DocumentReference',
'Appointment',
'CarePlan',
],
});Or configure interactions per resource:
FhirModule.forRoot({
resources: {
Patient: {
interactions: {
read: true,
create: true,
update: true,
delete: false,
search: true,
},
search: [
'name',
'identifier',
'birthdate',
'gender',
],
},
Observation: {
interactions: {
read: true,
create: true,
update: true,
delete: false,
search: true,
},
search: [
'patient',
'subject',
'code',
'date',
'category',
],
},
},
});The same configuration is used to generate the server's CapabilityStatement.
Framework Integrations
NestJS
npm install @simplefhir/nestjsimport { Module } from '@nestjs/common';
import { FhirModule } from '@simplefhir/nestjs';
@Module({
imports: [
FhirModule.forRoot({
version: 'R4',
basePath: '/fhir',
}),
],
})
export class AppModule {}Async configuration is also supported:
FhirModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
version: 'R4',
basePath: config.get('FHIR_BASE_PATH') ?? '/fhir',
}),
});Express
npm install @simplefhir/express expressimport express from 'express';
import { fhir } from '@simplefhir/express';
const app = express();
app.use(
'/fhir',
fhir({
version: 'R4',
resources: [
'Patient',
'Observation',
'Encounter',
'Condition',
],
}),
);
app.listen(3000, () => {
console.log('SimpleFHIR running at http://localhost:3000/fhir/metadata');
});Fastify
npm install @simplefhir/fastify fastifyimport Fastify from 'fastify';
import { fhirPlugin } from '@simplefhir/fastify';
async function start() {
const fastify = Fastify({
logger: true,
});
await fastify.register(fhirPlugin, {
prefix: '/fhir',
version: 'R4',
resources: [
'Patient',
'Observation',
'Encounter',
],
});
await fastify.listen({
port: 3000,
host: '0.0.0.0',
});
}
void start();Development Storage
The default/in-memory storage adapter is intended for:
- quick starts;
- examples;
- local development;
- unit and integration tests.
FhirModule.forRoot()No external database is required for the simplest local setup.
Do not use in-memory storage for production healthcare data.
Production Persistence
PostgreSQL + Prisma
npm install @simplefhir/nestjs @simplefhir/prisma @prisma/client
npm install -D prismaimport { Module } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { FhirModule } from '@simplefhir/nestjs';
import { prismaAdapter } from '@simplefhir/prisma';
const prisma = new PrismaClient();
@Module({
imports: [
FhirModule.forRoot({
version: 'R4',
basePath: '/fhir',
storage: prismaAdapter(prisma),
}),
],
})
export class AppModule {}The Prisma adapter uses a hybrid persistence model:
FHIR Resource
│
├── Canonical resource JSON → PostgreSQL JSONB
│
└── Search parameter extraction
│
├── string indexes
├── token indexes
├── date indexes
└── reference indexesThis keeps the complete FHIR resource intact while allowing searchable fields to use database indexes.
Typical resource storage contains:
id
resource_type
version_id
resource_json
created_at
updated_at
deleted_at
tenant_idSearch indexes are maintained separately for FHIR search parameter types.
Direct PostgreSQL
If you prefer pg without Prisma:
npm install @simplefhir/postgres pg
npm install -D @types/pgimport { Pool } from 'pg';
import { postgresAdapter } from '@simplefhir/postgres';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const storage = postgresAdapter(pool);
await storage.initSchema();
FhirModule.forRoot({
storage,
});FHIR Search
SimpleFHIR parses FHIR search syntax into a database-independent Search AST.
Example request:
GET /fhir/Observation?patient=123&date=gt2026-01-01&_count=20Conceptually becomes:
{
resourceType: 'Observation',
filters: [
{
parameter: 'patient',
type: 'reference',
operator: 'eq',
value: '123',
},
{
parameter: 'date',
type: 'date',
operator: 'gt',
value: '2026-01-01',
},
],
pagination: {
count: 20,
},
}The storage adapter translates this representation into database-specific queries.
This prevents @simplefhir/core from being coupled to Prisma or PostgreSQL.
Search parameter types
SimpleFHIR's search architecture supports FHIR search types such as:
- string
- token
- reference
- date
- number
- quantity
- URI
Search responses are returned as FHIR Bundle resources with:
{
"resourceType": "Bundle",
"type": "searchset",
"total": 1,
"entry": []
}Validation
SimpleFHIR validates resources before persistence and returns FHIR OperationOutcome resources for validation errors.
Example:
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "invalid",
"diagnostics": "Patient.gender contains an invalid value"
}
]
}Validation is designed as a replaceable subsystem so applications can use progressively stronger validation, including profiles, FHIRPath-based invariants, and terminology validation.
Versioning and History
FHIR resources can carry:
{
"meta": {
"versionId": "3",
"lastUpdated": "2026-09-02T12:00:00Z"
}
}Updates increment meta.versionId.
History support is designed around immutable resource versions so previous resource states can be retained rather than overwritten.
Typical history routes include:
GET /fhir/Patient/:id/_history
GET /fhir/Patient/:id/_history/:versionTransactions and Batch Bundles
SimpleFHIR supports FHIR Bundle-based operations such as transactions and batches.
Example transaction:
curl -X POST http://localhost:3000/fhir \
-H "Content-Type: application/fhir+json" \
-d '{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"request": {
"method": "POST",
"url": "Patient"
},
"resource": {
"resourceType": "Patient",
"name": [
{
"family": "Doe"
}
]
}
}
]
}'For transactional storage adapters, transaction entries are executed atomically.
Lifecycle Hooks
SimpleFHIR exposes hooks for application-specific behavior without putting business logic into the FHIR core.
import { defineConfig } from '@simplefhir/core';
export default defineConfig({
version: 'R4',
basePath: '/fhir',
hooks: {
beforeCreate: async (resource, context) => {
console.log(`Creating ${resource.resourceType}`);
},
afterCreate: async (resource, context) => {
await eventBus.publish(
'fhir.resource.created',
resource,
);
},
beforeDelete: async (resourceType, id, context) => {
console.log(`Deleting ${resourceType}/${id}`);
},
},
});Supported lifecycle stages include:
beforeRequest / afterRequest
beforeCreate / afterCreate
beforeRead / afterRead
beforeUpdate / afterUpdate
beforeDelete / afterDelete
beforeSearch / afterSearch
beforeValidate / afterValidateAuthorization
SimpleFHIR does not force your application to adopt a specific authentication provider.
Your application can continue using:
- Passport
- JWT
- Auth0
- Keycloak
- Clerk
- Firebase
- Microsoft Entra ID / Azure AD
- custom authentication
SimpleFHIR receives the authenticated user/context and performs authorization through a framework-independent hook:
FhirModule.forRoot({
authorization: async ({
user,
resourceType,
interaction,
}) => {
if (!user) {
return {
allowed: false,
};
}
if (
resourceType === 'Patient' &&
interaction === 'delete'
) {
return {
allowed: false,
};
}
return {
allowed: true,
};
},
});Denied FHIR requests are returned as OperationOutcome responses.
SMART on FHIR
Install:
npm install @simplefhir/smartThe SMART package provides helpers for:
- SMART discovery;
- OAuth2 / OIDC integration;
- SMART scopes;
- patient context;
- encounter context;
- patient-compartment enforcement;
- EHR and standalone launch workflows.
Example:
import {
createSmartAuthorization,
SmartConfigBuilder,
} from '@simplefhir/smart';
const smartConfiguration = SmartConfigBuilder.build({
issuer: 'https://auth.hospital.org',
authorizationUrl: 'https://auth.hospital.org/oauth/authorize',
tokenUrl: 'https://auth.hospital.org/oauth/token',
});
const authorization = createSmartAuthorization({
enforcePatientCompartment: true,
});SimpleFHIR should integrate with an existing identity provider rather than encouraging applications to build an insecure custom OAuth server.
Terminology
Install:
npm install @simplefhir/terminologyimport {
createTerminologyService,
} from '@simplefhir/terminology';
const terminology = createTerminologyService();
const lookup = await terminology.lookup(
'http://loinc.org',
'883-9',
);
const expansion = await terminology.expand(
'http://hl7.org/fhir/ValueSet/administrative-gender',
);
const result = await terminology.validateCode({
valueSetUrl:
'http://hl7.org/fhir/ValueSet/administrative-gender',
code: 'female',
});Supported terminology capabilities include:
$lookup
$expand
$validate-codeTerminology providers are designed to be replaceable so applications can use local terminology data or an external terminology service.
HL7 v2 → FHIR
Install:
npm install @simplefhir/hl7v2import {
convertHL7v2ToFhir,
} from '@simplefhir/hl7v2';
const message = `MSH|^~\\&|HOSPITAL|ADT|RECEIVER|DEST|20260902120000||ADT^A01|MSG001|P|2.5
PID|1||MRN98765^^^HOSPITAL||DOE^JOHN^A||19800515|M
PV1|1|I|ROOM101^^BED1||||12345^SMITH^ALICE^MD`;
const resources = convertHL7v2ToFhir(message);The HL7 v2 package focuses on parsing and transformation, while persistence remains the responsibility of the configured FHIR runtime/storage adapter.
C-CDA → FHIR
Install:
npm install @simplefhir/cdaimport fs from 'node:fs';
import {
convertCdaToFhir,
} from '@simplefhir/cda';
const xml = fs.readFileSync(
'continuity-of-care-document.xml',
'utf8',
);
const resources = convertCdaToFhir(xml);The CDA package is intended for importing clinical document content into FHIR resources such as Patient, DocumentReference, Condition, AllergyIntolerance, and Observation, depending on the source document.
FHIR Bulk Data Export
Install:
npm install @simplefhir/bulk-dataimport {
BulkExportJobManager,
} from '@simplefhir/bulk-data';
const manager = new BulkExportJobManager({
storage: yourStorageAdapter,
baseUrl: 'https://api.hospital.org/fhir',
});
const exportJob = await manager.startExport({
requestUrl: '/fhir/$export',
resourceTypes: [
'Patient',
'Observation',
],
});The Bulk Data package is designed around asynchronous export jobs and NDJSON output.
CLI
Install or run through npx:
npx simplefhir initUseful commands:
# Create a SimpleFHIR configuration file
npx simplefhir init
# Validate a FHIR R4 JSON resource
npx simplefhir validate patient.json
# Inspect extracted search indexes
npx simplefhir inspect observation.jsonConfiguration
A fuller configuration can look like this:
import {
defineConfig,
} from '@simplefhir/core';
export default defineConfig({
version: 'R4',
basePath: '/fhir',
resources: {
Patient: {
interactions: {
read: true,
create: true,
update: true,
delete: false,
search: true,
},
search: [
'name',
'gender',
'birthdate',
'identifier',
],
},
},
hooks: {
beforeCreate: async (resource) => {
console.log(
`Creating ${resource.resourceType}`,
);
},
},
defaultPageCount: 20,
maxPageCount: 100,
strictValidation: true,
});The configuration acts as a shared source of truth for:
- enabled resources;
- resource interactions;
- supported search parameters;
- validation behavior;
- routing;
- authorization;
- lifecycle hooks;
- generated
CapabilityStatement.
Example Applications
| Example | Stack |
|---|---|
| examples/nestjs-basic | NestJS + in-memory storage |
| examples/nestjs-prisma | NestJS + PostgreSQL + Prisma |
| examples/express-basic | Express + SimpleFHIR |
| examples/fastify-basic | Fastify + SimpleFHIR |
| examples/smart-on-fhir | SMART on FHIR authorization |
Typical usage:
cd examples/nestjs-basic
pnpm install
pnpm startDevelopment
Install dependencies:
pnpm installBuild all packages:
pnpm buildRun tests:
pnpm test:unitDesign Principles
1. FHIR stays FHIR
SimpleFHIR simplifies configuration and infrastructure. It does not replace FHIR resources with a proprietary data model.
2. Simple defaults, advanced escape hatches
The simplest setup should remain:
FhirModule.forRoot()Advanced applications can configure storage, validation, resources, authorization, hooks, terminology, and interoperability modules.
3. Framework-independent core
@simplefhir/core must not import NestJS, Express, Fastify, Prisma, or PostgreSQL.
4. Database-independent search
FHIR search is parsed into a Search AST before storage-specific adapters translate it into database queries.
5. Existing authentication stays in place
SimpleFHIR integrates with the application's identity and authentication system rather than replacing it.
6. Production boundaries are explicit
A development server can be started with minimal configuration. Production deployments still require deliberate decisions around persistence, identity, authorization, audit, terminology, backups, observability, and infrastructure security.
Security and Compliance
SimpleFHIR provides infrastructure for implementing secure FHIR APIs, but using SimpleFHIR does not automatically make an application compliant with HIPAA, GDPR, MDR, or any other regulatory framework.
Production healthcare deployments should consider, at minimum:
- TLS;
- strong authentication;
- resource-level authorization;
- SMART scopes where applicable;
- tenant isolation;
- audit logging;
- encrypted secrets;
- secure database access;
- backup and recovery;
- data retention requirements;
- protected health information in application logs;
- deployment-region and data-residency requirements.
Resource bodies should not be written to ordinary application logs by default.
Project Direction
SimpleFHIR is designed to grow in three layers:
Phase 1
Simple FHIR Server
↓
CRUD + Search + Validation
CapabilityStatement
OperationOutcome
Persistence
Phase 2
Reusable FHIR Runtime
↓
History + Transactions
Advanced Search
Profiles + FHIRPath
Terminology
Subscriptions
Multi-tenancy
Phase 3
Interoperability Platform
↓
SMART on FHIR
Bulk Data
HL7 v2
C-CDA
Developer tooling
ObservabilityThe product philosophy remains the same at every layer:
Simple on the outside. FHIR on the wire. Extensible underneath.
Contributing
Contributions are welcome.
Before opening a large pull request:
- open an issue describing the change;
- keep FHIR-specific logic inside
@simplefhir/core; - keep framework/database-specific behavior inside adapters;
- include tests for public behavior;
- avoid introducing breaking public APIs without discussion.
