@nirmiteeio/fhir-sdk
v1.0.2
Published
Universal FHIR SDK for SMART on FHIR integrations. Supports Epic, Cerner, Allscripts, Athena Health, and more. Framework-agnostic with React hooks included.
Maintainers
Readme
@nirmitee/fhir-sdk
Universal FHIR SDK for SMART on FHIR integrations. Framework-agnostic with first-class React support.
Features
- Multi-EMR Support - Pre-configured for Epic, Cerner, Athena Health, Allscripts, NextGen, Meditech, and eClinicalWorks
- SMART on FHIR Compliant - Full OAuth 2.0 with PKCE support
- Type-Safe - Complete TypeScript definitions for FHIR R4 resources
- Framework Agnostic - Works with React, Vue, Angular, Svelte, or vanilla JavaScript
- React Hooks - Optional hooks for convenient React integration
- Auto Token Refresh - Handles token expiration automatically
- Batch Operations - Efficient parallel data fetching with controlled concurrency
- Pagination Support - Automatic pagination for large result sets
- Modular - Use only what you need
- Extensible - Easy to add custom EMR providers
- Well Tested - 72% code coverage with 128 tests
Installation
npm install @nirmitee/fhir-sdkFor React projects:
npm install @nirmitee/fhir-sdk react@^18Quick Start
React (Recommended)
The easiest way to get started with React:
import { useFHIR } from '@nirmitee/fhir-sdk/hooks';
function PatientDashboard() {
const {
// Authentication
isAuthenticated,
login,
logout,
// Patient data (auto-fetched)
patient,
medications,
vitals,
appointments,
isDataLoading,
refetch,
} = useFHIR('epic'); // or 'cerner', 'athena', 'allscripts'
if (!isAuthenticated) {
return <button onClick={login}>Login with Epic</button>;
}
if (isDataLoading) return <div>Loading patient data...</div>;
return (
<div>
<h1>Welcome {patient?.name?.[0]?.given?.[0]}!</h1>
<button onClick={logout}>Logout</button>
<button onClick={refetch}>Refresh Data</button>
<h2>Medications ({medications.length})</h2>
{medications.map(med => (
<div key={med.id}>
{med.medicationCodeableConcept?.text}
</div>
))}
<h2>Vitals ({vitals.length})</h2>
{vitals.map(vital => (
<div key={vital.id}>
{vital.code.text}: {vital.valueQuantity?.value} {vital.valueQuantity?.unit}
</div>
))}
</div>
);
}Vanilla JavaScript / TypeScript
Framework-agnostic usage:
import { SMARTAuthClient, FHIRClient, PatientService } from '@nirmitee/fhir-sdk';
// 1. Initialize auth client
const authClient = new SMARTAuthClient('epic');
// 2. Initiate login
const authUrl = await authClient.authorize();
window.location.href = authUrl;
// 3. Handle callback (on redirect page)
await authClient.handleCallback(window.location.href);
// 4. Create FHIR client
const fhirClient = new FHIRClient({
providerId: 'epic',
authClient: authClient,
autoRefreshToken: true,
});
// 5. Fetch patient data
const patientService = new PatientService(fhirClient);
const patientId = await authClient.getPatientId();
const medications = await patientService.getMedications(patientId);Vue.js
<script setup>
import { ref, onMounted } from 'vue';
import { SMARTAuthClient, FHIRClient } from '@nirmitee/fhir-sdk';
const authClient = new SMARTAuthClient('epic');
const fhirClient = new FHIRClient({ providerId: 'epic', authClient });
const patient = ref(null);
const isAuthenticated = ref(false);
onMounted(async () => {
isAuthenticated.value = await authClient.isAuthenticated();
if (isAuthenticated.value) {
const patientId = await authClient.getPatientId();
patient.value = await fhirClient.read('Patient', patientId);
}
});
const login = async () => {
const authUrl = await authClient.authorize();
window.location.href = authUrl;
};
</script>
<template>
<button v-if="!isAuthenticated" @click="login">Login</button>
<div v-else>Welcome {{ patient?.name?.[0]?.given?.[0] }}!</div>
</template>Core Features
FHIR Client Operations
import { FHIRClient } from '@nirmitee/fhir-sdk';
const fhirClient = new FHIRClient({ providerId: 'epic', authClient });
// Read a specific resource
const patient = await fhirClient.read('Patient', 'patient-id');
// Search for resources
const medications = await fhirClient.search('MedicationRequest', {
patient: 'patient-id',
status: 'active',
});
// Search by patient
const vitals = await fhirClient.searchByPatient('Observation', 'patient-id', {
category: 'vital-signs',
_sort: '-date',
});Batch Operations
Efficiently fetch multiple resources in parallel:
// Fetch multiple resources at once
const results = await fhirClient.batchFetch([
{ resourceType: 'Patient', params: { id: 'patient-123' }, key: 'patient' },
{ resourceType: 'Observation', params: { patient: 'patient-123', category: 'vital-signs' }, key: 'vitals' },
{ resourceType: 'MedicationRequest', params: { patient: 'patient-123' }, key: 'meds' }
], {
parallel: true,
maxConcurrent: 3,
continueOnError: true
});
console.log(results.patient); // Patient resource
console.log(results.vitals); // Array of Observations
console.log(results.meds); // Array of MedicationRequestsFluent batch builder API:
const results = await fhirClient.batch()
.read('Patient', 'patient-123', 'patient')
.search('Observation', { patient: 'patient-123', category: 'vital-signs' }, 'vitals')
.search('MedicationRequest', { patient: 'patient-123' }, 'meds')
.execute({ maxConcurrent: 3 });Pagination Support
Automatically handle large result sets:
// Fetch all pages automatically
const result = await fhirClient.getAllResources('Observation', {
patient: 'patient-123',
category: 'laboratory'
}, {
maxPages: 10,
maxResources: 500,
onProgress: (page, total) => console.log(`Page ${page}, Total: ${total}`)
});
console.log(`Fetched ${result.totalFetched} resources`);
console.log(result.resources);Memory-efficient iterator pattern:
// Process pages one at a time
for await (const page of fhirClient.iterateResources('Observation', {
patient: 'patient-123'
}, 100)) {
console.log(`Processing ${page.length} observations`);
for (const observation of page) {
await processObservation(observation);
}
}Request/Response Interceptors
Add custom logic to all requests:
// Add custom headers
fhirClient.addRequestInterceptor(async (url, options) => {
return {
url,
options: {
...options,
headers: {
...options.headers,
'X-Custom-Header': 'value',
},
},
};
});
// Transform responses
fhirClient.addResponseInterceptor(async (response, data) => {
console.log('Response received:', data);
return data;
});Supported EMR Providers
| Provider | ID | PKCE | Refresh Token | Status |
|----------|-----|------|---------------|--------|
| Epic Systems | epic | Yes | Yes | Fully Tested |
| Cerner (Oracle Health) | cerner | No | Yes | Fully Tested |
| Athena Health | athena | Yes | Yes | Fully Tested |
| Allscripts | allscripts | No | No | Fully Tested |
| NextGen Healthcare | nextgen | Yes | - | Template |
| Meditech | meditech | Yes | - | Template |
| eClinicalWorks | eclinicalworks | Yes | - | Template |
Adding Custom EMR Provider
import { emrRegistry } from '@nirmitee/fhir-sdk';
emrRegistry.registerProvider({
id: 'custom-emr',
name: 'Custom EMR',
authUrl: 'https://custom-emr.com/oauth/authorize',
tokenUrl: 'https://custom-emr.com/oauth/token',
fhirBaseUrl: 'https://custom-emr.com/fhir/r4',
clientId: process.env.CUSTOM_EMR_CLIENT_ID,
redirectUri: process.env.REDIRECT_URI,
scopes: ['patient/*.read', 'openid'],
oauth: {
flow: 'authorization_code',
pkce: true,
},
quirks: {
acceptHeader: 'application/fhir+json',
patientIdLocation: 'token.patient',
},
});Environment Variables
# Epic
NEXT_PUBLIC_EPIC_CLIENT_ID=your-client-id
# Cerner
NEXT_PUBLIC_CERNER_CLIENT_ID=your-client-id
# Athena
NEXT_PUBLIC_ATHENA_CLIENT_ID=your-client-id
# Allscripts
NEXT_PUBLIC_ALLSCRIPTS_CLIENT_ID=your-client-id
# Common
NEXT_PUBLIC_REDIRECT_URI=http://localhost:3000/callbackAPI Reference
Core Classes
SMARTAuthClient
OAuth 2.0 authentication with PKCE support.
const authClient = new SMARTAuthClient(providerId, options?);
// Methods
await authClient.authorize(); // Generate auth URL
await authClient.handleCallback(url); // Handle OAuth callback
await authClient.getAccessToken(); // Get current token
await authClient.refreshAccessToken(); // Refresh token
await authClient.isAuthenticated(); // Check auth status
await authClient.getPatientId(); // Get patient ID
await authClient.logout(); // Clear sessionFHIRClient
FHIR resource operations with interceptor support.
const fhirClient = new FHIRClient({ providerId, authClient });
// Methods
await fhirClient.read<T>(resourceType, id);
await fhirClient.search<T>(resourceType, params);
await fhirClient.searchByPatient<T>(resourceType, patientId, params);
await fhirClient.batchFetch(requests, options);
await fhirClient.getAllResources<T>(resourceType, params, options);
fhirClient.iterateResources<T>(resourceType, params, delayMs);
fhirClient.addRequestInterceptor(interceptor);
fhirClient.addResponseInterceptor(interceptor);PatientService
High-level patient data operations.
const service = new PatientService(fhirClient);
// Methods
await service.getPatient(patientId);
await service.getMedications(patientId, options?);
await service.getVitals(patientId, options?);
await service.getLabReports(patientId, options?);
await service.getAppointments(patientId, options?);
await service.getEncounters(patientId, options?);
await service.getProcedures(patientId, options?);
await service.getAllPatientData(patientId);React Hooks
useFHIR(providerId)
All-in-one hook combining authentication and data fetching.
Returns:
{
// Authentication
isAuthenticated: boolean;
isAuthLoading: boolean;
patientId: string | null;
login: () => Promise<void>;
handleCallback: (url: string) => Promise<void>;
logout: () => Promise<void>;
// Clients
fhirClient: FHIRClient | null;
patientService: PatientService | null;
// Patient data
patient: Patient | null;
medications: MedicationRequest[];
vitals: Observation[];
labReports: Observation[];
appointments: Appointment[];
encounters: Encounter[];
procedures: Procedure[];
// State
isDataLoading: boolean;
errors: Record<string, string>;
refetch: () => Promise<void>;
}Composable Hooks
import { useAuth, useFHIRClient, usePatientService } from '@nirmitee/fhir-sdk/hooks';
const auth = useAuth('epic');
const fhirClient = useFHIRClient('epic', auth.authClient);
const patientService = usePatientService(fhirClient);Architecture
The SDK is built with modularity in mind:
Layer 5: React Hooks (Optional)
└─ useFHIR, useAuth, usePatientData
Layer 4: Services (Optional)
└─ PatientService (or create your own)
Layer 3: FHIR Client
└─ FHIRClient
Layer 2: Auth Client
└─ SMARTAuthClient
Layer 1: Foundation
└─ EMR Registry, Storage, PKCE UtilsUse only what you need! The SDK is framework-agnostic at its core, with React hooks as an optional convenience layer.
Error Handling
import { FHIRError } from '@nirmitee/fhir-sdk';
try {
const patient = await patientService.getPatient('patient-id');
} catch (error) {
if (error instanceof FHIRError) {
console.error('FHIR Error:', {
message: error.message,
statusCode: error.statusCode,
resourceType: error.resourceType,
providerId: error.providerId,
operationOutcome: error.operationOutcome,
});
}
}EMR-Specific Quirks Handled
The SDK automatically handles EMR-specific behaviors:
- Epic: Requires specific status filters for MedicationRequest and Appointment
- Cerner: Uses
application/jsoninstead ofapplication/fhir+json - Athena: Returns 403 for restricted resources (treated as not found)
- Athena: Patient ID extracted from JWT
id_tokenclaims - All: Different PKCE requirements and scope configurations
TypeScript Support
Full TypeScript support with FHIR R4 type definitions:
import type { Patient, Observation, MedicationRequest } from '@ahryman40k/ts-fhir-types/lib/R4';
const patient: Patient = await fhirClient.read<Patient>('Patient', 'patient-id');
const vitals: Observation[] = await fhirClient.search<Observation>('Observation', {
patient: 'patient-id',
category: 'vital-signs'
});Benefits
- 90% Less Boilerplate - No more manual OAuth flows, token management, or API calls
- Type Safety - Full TypeScript support with FHIR R4 types
- Maintainability - EMR-specific logic centralized in one place
- Extensibility - Add new EMRs via JSON configuration
- Testing - Easy to mock and test
- Framework Agnostic - Works with any JavaScript framework
- Performance - Efficient batch operations and pagination
Documentation
- Full README - Complete documentation
- Examples - Detailed usage examples
- Modularity Guide - Architecture details
- Migration Guide - Upgrading from older versions
Requirements
- Node.js >= 18.0.0
- For React projects: React >= 18.0.0
Testing
The SDK has comprehensive test coverage:
- 128 tests
- 72% code coverage
- Unit tests for all core functionality
- Integration tests for OAuth flows
Contributing
Contributions are welcome! Please visit our GitHub repository to:
- Report issues
- Submit pull requests
- Request features
- View contribution guidelines
License
MIT © 2025 Nirmitee
See LICENSE file for details.
Links
Support
For questions and support:
- Open an issue on GitHub
- Check the examples
- Read the full documentation
Made with ❤️ by Nirmitee
