npm package: @nirmiteeio/fhir-sdk
Description: Universal FHIR SDK for SMART on FHIR integrations. Supports Epic, Cerner, Allscripts, Athena Health, and more. Framework-agnostic with React hooks included.
Author: Nirmitee
Latest version: 1.0.2

# @nirmitee/fhir-sdk

[![npm version](https://img.shields.io/npm/v/@nirmitee/fhir-sdk.svg)](https://www.npmjs.com/package/@nirmitee/fhir-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)

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

```bash
npm install @nirmitee/fhir-sdk
```

**For React projects:**
```bash
npm install @nirmitee/fhir-sdk react@^18
```

## Quick Start

### React (Recommended)

The easiest way to get started with React:

```tsx
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:

```typescript
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

```vue
<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

```typescript
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:

```typescript
// 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 MedicationRequests
```

Fluent batch builder API:

```typescript
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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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

```typescript
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

```env
# 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/callback
```

## API Reference

### Core Classes

#### `SMARTAuthClient`

OAuth 2.0 authentication with PKCE support.

```typescript
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 session
```

#### `FHIRClient`

FHIR resource operations with interceptor support.

```typescript
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.

```typescript
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:**
```typescript
{
  // 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

```typescript
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 Utils
```

**Use only what you need!** The SDK is framework-agnostic at its core, with React hooks as an optional convenience layer.

## Error Handling

```typescript
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/json` instead of `application/fhir+json`
- **Athena**: Returns 403 for restricted resources (treated as not found)
- **Athena**: Patient ID extracted from JWT `id_token` claims
- **All**: Different PKCE requirements and scope configurations

## TypeScript Support

Full TypeScript support with FHIR R4 type definitions:

```typescript
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](https://github.com/nirmiteeio/fhir-sdk#readme) - Complete documentation
- [Examples](https://github.com/nirmiteeio/fhir-sdk/blob/main/EXAMPLES.md) - Detailed usage examples
- [Modularity Guide](https://github.com/nirmiteeio/fhir-sdk/blob/main/MODULARITY.md) - Architecture details
- [Migration Guide](https://github.com/nirmiteeio/fhir-sdk/blob/main/MIGRATION.md) - 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](https://github.com/nirmiteeio/fhir-sdk) to:
- Report issues
- Submit pull requests
- Request features
- View contribution guidelines

## License

MIT © 2025 Nirmitee

See [LICENSE](https://github.com/nirmiteeio/fhir-sdk/blob/main/LICENSE) file for details.

## Links

- [GitHub Repository](https://github.com/nirmiteeio/fhir-sdk)
- [NPM Package](https://www.npmjs.com/package/@nirmitee/fhir-sdk)
- [Issue Tracker](https://github.com/nirmiteeio/fhir-sdk/issues)
- [Documentation](https://github.com/nirmiteeio/fhir-sdk#readme)

## Support

For questions and support:
- Open an issue on [GitHub](https://github.com/nirmiteeio/fhir-sdk/issues)
- Check the [examples](https://github.com/nirmiteeio/fhir-sdk/blob/main/EXAMPLES.md)
- Read the [full documentation](https://github.com/nirmiteeio/fhir-sdk#readme)

---

Made with ❤️ by Nirmitee