fhir-server
v0.3.0
Published
FHIR R4 REST API Server
Readme
fhir-server
A high-performance FHIR R4 REST API server built on Fastify and fhir-engine.
Features
- ⚡ High Performance — Built on Fastify for maximum throughput
- 🔒 Secure by Default — Helmet security headers, CORS, rate limiting
- 🔐 JWT Authentication — Built-in JWT auth with access policies
- 📊 Full FHIR R4 REST API — Complete implementation of FHIR REST operations
- 🔍 Advanced Search — Full search parameter support with _include/_revinclude
- 📜 Resource History — Complete version history tracking
- 🔄 Subscriptions — Real-time resource change notifications
- 🎯 Validation — $validate operation with OperationOutcome
- 📚 Terminology — $expand, $lookup, $validate-code, CodeSystem tree
- 📦 IG Management — Import, index, and browse ImplementationGuide resources
- 🔌 Pluggable Engine — Works with any fhir-engine implementation
- 📝 Request Logging — Comprehensive request/response logging
- 🎨 Type-Safe — Full TypeScript support
Installation
npm install fhir-server fhir-engineQuick Start
import { FhirServer } from "fhir-server";
import { createFhirEngine } from "fhir-engine"; // Your engine implementation
// Create FHIR engine instance
const engine = await createFhirEngine({
// Your engine configuration
});
// Create and start server
const server = new FhirServer({
engine,
port: 3000,
host: "0.0.0.0",
cors: {
enabled: true,
origin: "*",
},
auth: {
enabled: true,
jwtSecret: "your-secret-key",
},
});
await server.start();
console.log(`FHIR server running at ${server.getAddress()}`);Configuration
Basic Configuration
import { FhirServer, type FhirServerOptions } from "fhir-server";
const options: FhirServerOptions = {
engine, // Required: FhirEngine instance
port: 3000, // Default: 3000
host: "0.0.0.0", // Default: '0.0.0.0'
logger: true, // Enable request logging
trustProxy: true, // If behind reverse proxy
};
const server = new FhirServer(options);CORS Configuration
const server = new FhirServer({
engine,
cors: {
enabled: true,
origin: ["https://app.example.com", "https://admin.example.com"],
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: ["Content-Type", "Authorization"],
exposedHeaders: ["Location", "ETag", "Last-Modified"],
},
});Rate Limiting
const server = new FhirServer({
engine,
rateLimit: {
enabled: true,
max: 100, // Max requests per window
timeWindow: 60000, // 1 minute window
skipOnError: false,
},
});Authentication & Authorization
const server = new FhirServer({
engine,
auth: {
enabled: true,
jwtSecret: process.env.JWT_SECRET,
jwtAlgorithm: "HS256",
requireAuth: true, // Require auth for all endpoints
publicPaths: ["/metadata"], // Paths that don't require auth
},
});FHIR REST API
The server implements the complete FHIR R4 REST API:
Metadata
GET /metadataReturns the server's CapabilityStatement.
CRUD Operations
# Create
POST /{resourceType}
Content-Type: application/fhir+json
# Read
GET /{resourceType}/{id}
# Update
PUT /{resourceType}/{id}
Content-Type: application/fhir+json
# Patch
PATCH /{resourceType}/{id}
Content-Type: application/json-patch+json
# Delete
DELETE /{resourceType}/{id}
# Conditional Create
POST /{resourceType}
If-None-Exist: identifier=12345
# Conditional Update
PUT /{resourceType}?identifier=12345
# Conditional Delete
DELETE /{resourceType}?status=inactiveSearch
# Type-level search
GET /{resourceType}?param=value
# System-level search
GET /?_type=Patient,Observation¶m=value
# Search with _include
GET /Patient?_include=Patient:organization
# Search with _revinclude
GET /Patient?_revinclude=Observation:subject
# Pagination
GET /Patient?_count=20&_offset=40
# Sorting
GET /Patient?_sort=birthdate
# Summary
GET /Patient?_summary=trueHistory
# Instance history
GET /{resourceType}/{id}/_history
# Type history
GET /{resourceType}/_history
# System history
GET /_history
# Specific version
GET /{resourceType}/{id}/_history/{vid}Batch/Transaction
POST /
Content-Type: application/fhir+json
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [...]
}Operations
# Validate resource
POST /{resourceType}/$validate
POST /{resourceType}/{id}/$validate
# Expand ValueSet
GET /ValueSet/$expand?url=http://...
POST /ValueSet/$expand
# Lookup code
GET /CodeSystem/$lookup?system=http://...&code=123
# Validate code
GET /ValueSet/$validate-code?url=http://...&code=123IG Management (v0.2.0)
# Get IG content index (profiles, extensions, valueSets, codeSystems)
GET /_ig/{igId}/index
# Get StructureDefinition with extracted dependencies
GET /_ig/{igId}/structure/{sdId}
# Batch load multiple resources as FHIR Collection Bundle
POST /_ig/{igId}/bundle
Content-Type: application/json
{ "resources": ["StructureDefinition/us-core-patient", "StructureDefinition/us-core-obs"] }
# Import an IG from a FHIR Bundle
POST /_admin/ig/import
Content-Type: application/json
{ "igId": "us-core", "bundle": { "resourceType": "Bundle", "type": "collection", "entry": [...] } }
# List all imported IGs
GET /_admin/ig/list
# CodeSystem concept tree (nested hierarchy)
GET /_terminology/codesystem/{id}/treeETag / Caching
Conformance resources (StructureDefinition, ValueSet, CodeSystem, etc.) include:
ETag: W/"{versionId}"headerCache-Control: max-age=3600, must-revalidateIf-None-Matchsupport →304 Not Modified
Authentication
JWT Token Format
The server expects JWT tokens in the Authorization header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Token Payload
{
sub: 'user-id',
email: '[email protected]',
roles: ['practitioner', 'admin'],
iat: 1234567890,
exp: 1234571490
}Access Policies
The server supports three-layer access control:
- System-level: Global permissions
- Resource-level: Per-resource-type permissions
- Instance-level: Per-resource-instance permissions
// Example: User can read all Patients, but only update their own
{
"resourceType": "AccessPolicy",
"resource": [
{
"resourceType": "Patient",
"interaction": ["read", "search"]
},
{
"resourceType": "Patient",
"criteria": "Patient?_id={{user.patientId}}",
"interaction": ["update"]
}
]
}Subscriptions
WebSocket Subscriptions
The server supports real-time subscriptions via WebSocket:
// Client-side
const ws = new WebSocket("ws://localhost:3000/ws");
ws.on("open", () => {
ws.send(
JSON.stringify({
type: "subscribe",
criteria: "Observation?status=final",
}),
);
});
ws.on("message", (data) => {
const event = JSON.parse(data);
console.log("Resource updated:", event.resource);
});Subscription Manager
import { SubscriptionManager } from "fhir-server";
const subscriptionManager = new SubscriptionManager({ engine });
subscriptionManager.on("notification", (event) => {
// Handle notification
console.log("Subscription triggered:", event);
});
// Evaluate resource against subscriptions
await subscriptionManager.evaluateResource({
resourceType: "Observation",
id: "obs-123",
status: "final",
});Error Handling
The server returns FHIR-compliant OperationOutcome resources for errors:
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "not-found",
"diagnostics": "Resource Patient/invalid-id not found"
}
]
}HTTP Status Codes
200 OK— Successful read/search201 Created— Successful create204 No Content— Successful delete400 Bad Request— Invalid request401 Unauthorized— Missing/invalid authentication403 Forbidden— Insufficient permissions404 Not Found— Resource not found409 Conflict— Version conflict410 Gone— Resource deleted422 Unprocessable Entity— Validation error429 Too Many Requests— Rate limit exceeded500 Internal Server Error— Server error
Middleware
Custom Middleware
You can register custom Fastify middleware:
import { FhirServer } from "fhir-server";
const server = new FhirServer({ engine });
// Access the underlying Fastify instance
server.app.addHook("onRequest", async (request, reply) => {
// Custom logic
console.log("Request:", request.method, request.url);
});
await server.start();Built-in Middleware
The server includes these middleware layers:
- Security Headers (Helmet)
- CORS
- Rate Limiting
- Request Logging
- Request Context (parses FHIR content-type)
- Authentication (JWT validation)
- Error Handler (converts to OperationOutcome)
Engine Interface
The server requires a FhirEngine implementation:
interface FhirEngine {
// CRUD
createResource(type: string, resource: Resource): Promise<PersistedResource>;
readResource(type: string, id: string): Promise<PersistedResource>;
updateResource(
type: string,
resource: PersistedResource,
): Promise<PersistedResource>;
deleteResource(type: string, id: string): Promise<void>;
// Search
search(
type: string,
params: Record<string, string>,
options?: SearchOptions,
): Promise<SearchResult>;
// History
historyInstance(
type: string,
id: string,
params?: Record<string, string>,
): Promise<Bundle>;
historyType(type: string, params?: Record<string, string>): Promise<Bundle>;
historySystem(params?: Record<string, string>): Promise<Bundle>;
// Validation
validate(resource: Resource): Promise<ValidationResult>;
// Metadata
status(): Promise<FhirEngineStatus>;
}TypeScript Support
Full TypeScript definitions included:
import type {
FhirServer,
FhirServerOptions,
FhirEngine,
Resource,
Bundle,
OperationOutcome,
} from "fhir-server";Performance
Built on Fastify for maximum performance:
- Throughput: 30,000+ req/sec (simple reads)
- Latency: <5ms p50, <20ms p99 (with in-memory engine)
- Memory: Efficient streaming for large bundles
- Concurrency: Handles thousands of concurrent connections
Deployment
Docker
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "dist/esm/index.mjs"]Environment Variables
PORT=3000
HOST=0.0.0.0
JWT_SECRET=your-secret-key
CORS_ORIGIN=https://app.example.com
RATE_LIMIT_MAX=100
LOG_LEVEL=infoLicense
Apache-2.0
Contributing
Contributions welcome! Please see CONTRIBUTING.md for details.
