TypeScript SDK
The Oystehr SDK is a TypeScript library for interacting with Oystehr's FHIR and Project APIs (opens in a new tab).
While you can always make raw HTTP requests to Oystehr's APIs from any language, this SDK provides several advantages for developers:
- TypeScript types — The SDK provides complete typing for requests and responses across all of Oystehr's APIs. This gives you helpful autocomplete and build-time type checking.
- Convenience functions — The SDK provides convenience functions for doing common tasks like uploading and downloading files with Z3.
- FHIR Support — The SDK supports the R4B and R5 versions of FHIR and includes types and helpers for interacting with the Oystehr FHIR API.
Installation
npm install @oystehr/sdk
Usage
- Import the SDK.
- Initialize the SDK with a Oystehr access token. (Learn how to get an access token (opens in a new tab)).
- Invoke any of Oystehr's APIs with a function call.
import Oystehr from '@oystehr/sdk';
import { Patient } from 'fhir/r4b';
const oystehr = new Oystehr({
accessToken: 'your_access_token',
});
// Create a Patient FHIR resource
const patient = await oystehr.fhir.create<Patient>({
resourceType: 'Patient',
});
// Upload a zambda
try {
const zambda = await oystehr.zambda.create({ name: 'new zambda', triggerMethod: 'http_open' });
const fileHandle = await fs.open('./path/to/my/zambda.zip')
const file = new Blob([await fileHandle.readFile()]);
await oystehr.zambda.uploadFile({ id: createZambda.data.id, file, })
} catch (err) {
// Handle error thrown by Oystehr or JS in some way
}
SDK Config Object
Instantiating the SDK requires a configuration object, though all fields are optional.
interface OystehrConfig {
/**
* Access token for your user, M2M, or developer account.
*/
accessToken?: string;
/**
* Optional Oystehr Project ID. Required for developer accessTokens.
*/
projectId?: string;
/**
* Optionally provide a custom fetch implementation. This must conform to the
* built-in node.js fetch implementation (Undici fetch).
* @param req
* @returns
*/
fetch?: (req: Request) => Promise<Response>;
/**
* Optional configuration for retrying request in case of error. Defaults to
* `{ retries: 3 }` if not provided.
*/
retry?: {
/**
* Number of retries.
*/
retries: number;
/**
* Optional, non-async function to call when a request is retried.
* @param attempt
* @returns
*/
onRetry?: (attempt: number) => void;
/**
* Optional list of additional errors to retry for. The Oystehr SDK will always
* retry for status codes 408, 429, 500, 502, 503, and 504.
*/
retryOn?: number[];
};
}
Features
Unauthenticated Usage
All Oystehr endpoints require authentication to access except for public Zambdas that you upload to your project. You can invoke these Zambdas without passing an authentication token to the SDK:
import Oystehr from '@oystehr/sdk';
const oystehr = new Oystehr({});
// Execute the Zambda
const result = await oystehr.zambda.executePublic({
id: 'your_oystehr_zambda_id',
additional: 'params',
});
// Do something with your Zambda result
console.log(JSON.stringify(result, null, 2));
Retries
The Oystehr SDK will retry on a variety of errors by default:
- Common Node.js and Undici networking errors
- Rate limiting status codes (
408
,429
) - Potentially ephemeral upstream status codes (
500
,502
,503
,504
)
Note that the Oystehr FHIR API is eventually consistent, so if you are retrieving or updating a resource immediately after creating it, it's recommended to retry on 404
and 412
status codes as well.
To retry on additional status codes, change the number of retries, or register a callback for each retry, pass options to the Oystehr SDK configuration object as shown above.
Types
Oystehr Types
The SDK includes input and output types for all non-FHIR Oystehr services, as well as some reusable "component" types. These types provide a single interface for inputs — no need to worry about whether a value goes into a path parameters or the request body.
import Oystehr, { UserInviteParams, UserInviteResponse } from '@oystehr/sdk';
// Hard-coded application ID from environment
const APP_ID = process.env.OYSTEHR_APP_ID;
// Elsewhere in your project, initialize an Oystehr client
// with the currently-authenticated user's access token
const oystehr = new Oystehr({ accessToken: accessTokenFromAuthn });
// Define a reusable helper function to add practitioners
// to your application, omitting the `applicationId` field
// since you will override it.
async function inviteUserToMyApp(
oystehr: Oystehr,
input: Omit<UserInviteParams, 'applicationId'>
): Promise<UserInviteResponse> {
return oystehr.user.invite({
...input,
applicationId: APP_ID,
});
}
FHIR
FHIR Types
The SDK supports both the R4B and R5 versions of FHIR from one shared set of generic functions. You shouldn't need to mix these types, since each Oystehr project can only have one FHIR version, but the example below shows how the SDK handles types for each version.
import Oystehr from '@oystehr/sdk';
import { Patient as PatientR4 } from 'fhir/r4b';
import { Patient as PatientR5 } from 'fhir/r5';
const oystehr = new Oystehr({
accessToken: 'your_access_token',
});
const encounter4 = await oystehr.fhir.create<EncounterR4B>({
resourceType: 'Encounter',
status: 'arrived',
class: {},
});
/*
* R4B Encounters do not have `virtualService` so this shows an error:
* Argument of type '{ resourceType: "Encounter"; status: "arrived"; class: {}; virtualService: {}; }' is not assignable to parameter of type 'Encounter'.
* Object literal may only specify known properties, and 'virtualService' does not exist in type 'Encounter'.ts(2345)
*/
const vs = encounter4.virtualService;
const encounter5 = await oystehr.fhir.create<EncounterR5>({
resourceType: 'Encounter',
/*
* R5 Encounters have different enum values for `status` so this shows an error:
* Type '"arrived"' is not assignable to type '"planned" | "in-progress" | "cancelled" | "entered-in-error" | "unknown" | "on-hold" | "completed" | "discharged" | "discontinued"'.ts(2322)
*/
status: 'arrived',
class: [],
});
FHIR Bundles
When performing a FHIR search, the resulting type is Bundle
. The Oystehr SDK has a convenience function for unwrapping the bundle into an array of the base type.
import Oystehr from '@oystehr/sdk';
import { Patient } from 'fhir/r4b';
const patientBundle/*: Bundle<Patient>*/ = await oystehr.fhir.search<Patient>({
resourceType: 'Patient',
params: [
{
name: 'name',
value: 'sam',
},
],
});
const patients/*: Patient[]*/ = patientBundle.unbundle();
Batch Requests
The Oystehr FHIR service supports two kinds of batch requests (opens in a new tab): batch and transaction. In the SDK, the syntax to use these is the same.
PATCH requests behave differently in batch requests (opens in a new tab). The Oystehr SDK allows you to pass either pre-encoded Binary
resources or JSON Patch operations directly.
Here's an example of a transaction that patches an appointment and then retrieves a list of today's appointments:
import dayjs from 'dayjs';
import Oystehr, { BatchInputGetRequest, BatchInputPatchRequest } from '@oystehr/sdk';
const oystehr = new Oystehr({
accessToken: '<your_access_token>',
});
const patchAppointment: BatchInputPatchRequest = {
method: 'PATCH',
url: '/Appointment/some_appointment_id',
operations: [{
op: 'replace',
path: '/start',
value: dayjs().format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
}],
};
const getAppointments: BatchInputGetRequest = {
method: 'GET',
url: `/Appointment?date=sa${dayjs().startOf('day').format('YYYY-MM-DDTHH:mm:ss.SSSZ')}`,
};
await oystehr.fhir.transaction({ requests: [patchAppointment, getAppointments ]});
Optimistic Locking
The Oystehr FHIR API and SDK supports FHIR's optimistic locking specification (opens in a new tab). To use optimistic locking, pass a valid FHIR version ID to the request object on your SDK function call.
import Oystehr from '@oystehr/sdk';
import { Patient } from 'fhir/r4b';
const oystehr = new Oystehr({
accessToken: 'your_access_token',
});
// Create a Patient FHIR resource
const patient = await oystehr.fhir.create<Patient>({
resourceType: 'Patient',
});
// Fails if patient has been updated by a separate process
const updatedPatient = await oystehr.fhir.update<Patient>({
resourceType: 'Patient'
id: patient.id,
active: true,
}, { optimisticLockingVersionId: patient.meta?.versionId });
Convenience Functions
Z3
Under the hood, uploading and downloading files with Z3 is a two-step process:
- Create a presigned URL to upload or download the file (API Reference (opens in a new tab)).
- Upload or download the file using the presigned URL.
The SDK provides convenience functions which combine these steps into a single function call to make it a one-liner:
await oystehr.z3.uploadFile(
{ bucketName: 'your-bucket-name', 'objectPath+': 'path/to/your-filename', file: someFileBlob }
);
In the example, someFileBlob
is a Blob (opens in a new tab).
const downloadedBuffer = await oystehr.z3.downloadFile(
{ bucketName: 'your-bucket-name', 'objectPath+': 'path/to/your-filename' }
);
Zambda
Uploading code for your Zambda Functions is very similar to uploading a file with Z3. After creating your .zip
archive, use the uploadFile()
function to deploy your code.
const zambdaFile = fs.readFileSync('build/your-zambda-code.zip');
await oystehr.zambda.uploadFile({
id: yourZambdaId,
file: new Blob([zambdaFile]),
});
Migration Guide
If you have questions about migrating to a new version of the Oystehr SDK, check out our migration guide (opens in a new tab).
Contact
Questions? Join our public Slack (opens in a new tab) or contact support.