Core Documentation
TypeScript SDK

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

  1. Import the SDK.
  2. Initialize the SDK with a Oystehr access token. (Learn how to get an access token (opens in a new tab)).
  3. Invoke any of Oystehr's APIs with a function call.
Example of using the SDK
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:

Example of unauthenticated usage
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.

Example of a reusable user invite wrapper
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.

Example of using FHIR types with the SDK
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:

  1. Create a presigned URL to upload or download the file (API Reference (opens in a new tab)).
  2. 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:

Upload a file to Z3 with the SDK
    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).

Download a file from Z3 with the SDK
    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.

Deploy a code zip to a Zambda Function
  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.