@fhirust/sdk
v0.4.2
Published
TypeScript SDK for building FHIRust WASM plugins
Maintainers
Readme
@fhirust/sdk
TypeScript SDK for building FHIRust WASM plugins
Build powerful FHIR resource validation, transformation, and enrichment plugins for FHIRust using TypeScript and WebAssembly.
📦 Installation
npm install @fhirust/sdk @bytecodealliance/componentize-jsRequirements:
- Node.js 18+ or Bun
- TypeScript 5.0+
@bytecodealliance/componentize-js>= 0.14.0 (0.19+ recommended for security fixes)
🚀 Quick Start
1. Create a new plugin
import { FhirPlugin, reject } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";
const plugin = new FhirPlugin("my-validator", "1.0.0");
// Validate Patient before creation
plugin.beforeCreate<Patient>("Patient", (patient, ctx) => {
if (!patient.name || patient.name.length === 0) {
return reject("Patient must have at least one name");
}
return patient;
});
// Export plugin hooks
const pluginExports = plugin.exports();
export const lifecycle = pluginExports.lifecycle;
export const hooks = pluginExports.hooks;2. Build the plugin
# Using the SDK CLI
npx fhirust-sdk build
# Or manually with componentize-js
npx jco componentize src/index.ts -o dist/plugin.wasm \
--wit wit/plugin.wit \
--world-name fhirust-plugin3. Deploy to FHIRust server
# config/server.toml
[[plugins.plugins]]
name = "my-validator"
path = "./plugins/my-validator.wasm"
enabled = true✨ Features
- Type-safe FHIR resources — Full TypeScript types for FHIR R4 resources
- Hook-based architecture — Intercept and modify resources before/after CRUD operations
- Built-in FHIR client — Make authenticated API calls to the FHIR server
- HTTP client — Fetch data from external APIs with allowlist control
- Event emission — Send async events for logging, webhooks, and integrations
- WebAssembly runtime — Sandboxed execution with fine-grained permissions
- Testing utilities — Built-in test harness for unit testing plugins
📚 API Reference
Core Classes
FhirPlugin
Main plugin class for registering hooks.
import { FhirPlugin } from "@fhirust/sdk";
const plugin = new FhirPlugin(name: string, version: string);Methods:
beforeCreate<T>(resourceType, handler)— Hook before resource creationafterCreate<T>(resourceType, handler)— Hook after resource creationbeforeRead(resourceType, handler)— Hook before resource readafterRead<T>(resourceType, handler)— Hook after resource readbeforeUpdate<T>(resourceType, handler)— Hook before resource updateafterUpdate<T>(resourceType, handler)— Hook after resource updatebeforeDelete(resourceType, handler)— Hook before resource deletionafterDelete(resourceType, handler)— Hook after resource deletionexports()— Export plugin metadata and hook executor
Helper Functions
reject(message: string): never
Reject a hook operation with an error message.
if (!patient.birthDate) {
return reject("Patient birth date is required");
}outcome(issues: OperationOutcomeIssue[]): OperationOutcome
Create a FHIR OperationOutcome for complex validation errors.
return outcome([
{ severity: "error", code: "required", diagnostics: "Missing name" },
{ severity: "warning", code: "business-rule", diagnostics: "Unusual age" }
]);FHIR Client
Access the FHIR server from within your plugin using plugin.fhir.
plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
// Search for duplicates
const bundle = await plugin.fhir.search("Patient", {
identifier: patient.identifier?.[0]?.value
});
if (bundle.total && bundle.total > 0) {
return reject("Duplicate patient identifier");
}
return patient;
});Methods:
plugin.fhir.search(resourceType, params)— Search for resourcesplugin.fhir.read(resourceType, id)— Read a resource by IDplugin.fhir.create(resourceType, resource)— Create a new resourceplugin.fhir.update(resourceType, id, resource)— Update a resourceplugin.fhir.delete(resourceType, id)— Delete a resource
HTTP Client
Make HTTP requests to external APIs using plugin.http (requires http.fetch permission).
plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
// Verify insurance eligibility via external API
const response = await plugin.http.post("https://api.insurance.com/verify", {
body: { memberId: patient.identifier?.[0]?.value }
});
if (!response.ok) {
return reject("Insurance verification failed");
}
return patient;
});Event Emitter
Emit events for async processing using plugin.events.
plugin.afterCreate<Patient>("Patient", (patient, ctx) => {
plugin.events.emit("patient.created", {
id: patient.id,
name: patient.name?.[0]?.family
});
return patient;
});Logger
Structured logging with severity levels using plugin.log.
plugin.beforeCreate<Patient>("Patient", (patient, ctx) => {
plugin.log.info(`Validating patient: ${patient.id}`);
plugin.log.warn(`Missing phone number for patient: ${patient.id}`);
plugin.log.error(`Validation failed for patient: ${patient.id}`);
return patient;
});Note: Logger methods accept only string messages. To include structured data, serialize it in the message string using template literals or JSON.stringify().
💡 Examples
Example 1: Patient Name Validator
import { FhirPlugin, reject } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";
const plugin = new FhirPlugin("name-validator", "1.0.0");
plugin.beforeCreate<Patient>("Patient", (patient) => {
if (!patient.name || patient.name.length === 0) {
return reject("Patient must have at least one name");
}
const hasFamily = patient.name.some(n => n.family);
if (!hasFamily) {
return reject("At least one name must include a family name");
}
return patient;
});
export const { lifecycle, hooks } = plugin.exports();Example 2: Auto-Tag Resources
import { FhirPlugin } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";
const plugin = new FhirPlugin("auto-tagger", "1.0.0");
plugin.beforeCreate<Patient>("Patient", (patient) => {
// Add validation tag
patient.meta = patient.meta || {};
patient.meta.tag = patient.meta.tag || [];
patient.meta.tag.push({
system: "http://example.org/tags",
code: "validated",
display: "Validated by Plugin"
});
return patient;
});
export const { lifecycle, hooks } = plugin.exports();Example 3: Duplicate Detection
import { FhirPlugin, reject } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";
const plugin = new FhirPlugin("duplicate-detector", "1.0.0");
plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
const identifier = patient.identifier?.[0]?.value;
if (!identifier) return patient;
try {
// Search for existing patients with same identifier
const bundle = await plugin.fhir.search("Patient", {
identifier: identifier
});
if (bundle.total && bundle.total > 0) {
return reject(`Duplicate patient found with identifier: ${identifier}`);
}
return patient;
} catch (error) {
plugin.log.error(`Duplicate check failed: ${error}`);
// Fail open: allow creation if duplicate check fails
return patient;
}
});
export const { lifecycle, hooks } = plugin.exports();Example 4: External API Integration
import { FhirPlugin, reject } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";
const plugin = new FhirPlugin("insurance-verifier", "1.0.0");
plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
const memberId = patient.identifier?.find(
i => i.system === "http://insurance.org"
)?.value;
if (memberId) {
try {
// Verify with external insurance API
const response = await plugin.http.post(
"https://api.insurance.com/verify",
{ body: { memberId } }
);
if (!response.ok) {
return reject("Insurance verification failed");
}
plugin.log.info(`Insurance verified for member: ${memberId}`);
} catch (error) {
plugin.log.error(`Insurance verification error: ${error}`);
return reject("Insurance verification service unavailable");
}
}
return patient;
});
export const { lifecycle, hooks } = plugin.exports();🧪 Testing
Use the built-in testing utilities:
import { FhirPlugin } from "@fhirust/sdk";
import { createTestContext } from "@fhirust/sdk/testing";
import type { Patient } from "@fhirust/sdk/r4";
import { test } from "node:test";
import assert from "node:assert";
test("validates patient name", async () => {
const plugin = new FhirPlugin("test", "1.0.0");
plugin.beforeCreate<Patient>("Patient", (patient) => {
if (!patient.name?.length) {
return reject("Name required");
}
return patient;
});
const ctx = createTestContext();
const patient: Patient = {
resourceType: "Patient",
name: [{ family: "Doe", given: ["John"] }]
};
const handler = plugin.getHandler("beforeCreate", "Patient");
const result = await handler(patient, ctx);
assert.equal(result.name[0].family, "Doe");
});Run tests:
npm test🔧 CLI Commands
The SDK includes a CLI for common tasks:
# Create a new plugin from template
npx fhirust-sdk init my-plugin
# Build plugin to WASM
npx fhirust-sdk build
# Run tests
npx fhirust-sdk test
# Validate plugin manifest
npx fhirust-sdk validate📝 Configuration
Plugin Permissions
Configure plugin permissions in config/server.toml:
[[plugins.plugins]]
name = "my-plugin"
path = "./plugins/my-plugin.wasm"
enabled = true
permissions = [
"fhir.read", # Read FHIR resources
"fhir.write", # Create/update FHIR resources
"utils.log", # Write to server logs
"utils.emit-event", # Emit async events
"http.fetch" # Make HTTP requests
]
http_allowlist = [
"https://api.insurance.com/*",
"https://api.eligibility.org/*"
]🤝 Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
📄 License
MIT © FHIRust Contributors
🔗 Resources
- Documentation: https://fhirust.dev/docs/plugins
- GitHub: https://github.com/wei6bin/fhirust
- npm: https://www.npmjs.com/package/@fhirust/sdk
- Issues: https://github.com/wei6bin/fhirust/issues
📌 Version History
See CHANGELOG.md for release notes.
