# How to build a healthcare API with TypeScript

> Tutorial: an Express API in TypeScript that creates patient records, stores blood-pressure readings as FHIR R4 observations, reads them back, maps errors and verifies webhooks — without a medical database of your own.

Source: https://developers.anpheros.com/guides/healthcare-api-typescript

**This tutorial builds the backend API of a blood-pressure tracking app in TypeScript with Express: sign-up creates the user's medical record, readings are stored as coded HL7 FHIR R4 observations, the app reads them back, and a webhook endpoint hears about new data, with no medical database of your own.** Your API keeps what is specific to your product (users, sessions, settings). The medical data, its history, consent and provenance live in Anpheros, which you call through the [TypeScript SDK](https://developers.anpheros.com/guides/sdks).

## Architecture

```
 mobile / web app ──► your API (Express, TypeScript) ──► Anpheros (FHIR R4 record per patient)
                       │  users, sessions, settings        │  observations, conditions, documents,
                       │  user id → patient id              │  provenance, consent, audit
                       ◄──────────── webhooks ──────────────┘
```

Your database stores one extra column per user: their Anpheros patient id. Everything clinical goes through the API, so it is coded, versioned and readable by any FHIR system or by the patient's other apps they consent to. [Medical app backend and database](https://developers.anpheros.com/guides/medical-app-backend) explains the split in detail.

## Before you start

- Node.js 18 or newer.
- A sandbox key (`sk_test_…`) from [platform.anpheros.com/dashboard](https://platform.anpheros.com/dashboard/) (**Get a sandbox key**). Keep it on the server only; it must never reach the app.

```bash
mkdir bp-api && cd bp-api
npm init -y && npm pkg set type=module
npm install express @anpheros/sdk
npm install -D typescript tsx @types/express @types/node
export ANPHEROS_KEY=sk_test_…
```

## Step 1: the client and your user store

```ts
// server.ts
import express from 'express';
import { Anpheros, AnpherosError, apiKey, verifyWebhookSignature } from '@anpheros/sdk';

const anpheros = new Anpheros({ auth: apiKey(process.env.ANPHEROS_KEY!) });
const app = express();

// Your own user table. In production this is your database; the only medical link is patientId.
type User = { id: string; email: string; patientId: string };
const users = new Map<string, User>();

// Stand-in for your real authentication (session cookie, JWT…): it must set the signed-in user.
function currentUser(req: express.Request): User {
  const user = users.get(req.header('x-user-id') ?? '');
  if (!user) throw new HttpError(401, 'not signed in');
  return user;
}

class HttpError extends Error {
  constructor(readonly status: number, message: string) { super(message); }
}
```

## Step 2: sign-up creates the medical record

When someone signs up, create their patient in Anpheros and put your own user id in `identifiers`, so the record can always be found from your side:

```ts
app.post('/signup', express.json(), async (req, res, next) => {
  try {
    const { email, given, family, birthDate } = req.body;
    const userId = crypto.randomUUID();
    const patient = await anpheros.patients.create(
      { given, family, birth_date: birthDate, identifiers: [{ system: 'https://bp.example/user', value: userId }] },
      { author_type: 'patient' },
    );
    users.set(userId, { id: userId, email, patientId: patient.id });
    res.status(201).json({ userId });
  } catch (e) { next(e); }
});
```

The response from Anpheros is the new patient, with an `id` that belongs to your project and its FHIR reference:

```json
{"id": "27296656-c2ba-4970-8424-1dcd91163e0a", "given": "Ioana", "family": "Marin", "birth_date": "1979-04-12", "gender": "female",
 "identifiers": [{"system": "https://bp.example/user", "value": "u_8f2c"}], "updated_at": "2026-10-12T20:16:51.143Z",
 "fhir": "Patient/27296656-c2ba-4970-8424-1dcd91163e0a"}
```

## Step 3: store a reading

The app sends `{systolic, diastolic, measuredAt}`. Your API validates it and stores one blood-pressure observation, LOINC `85354-9`, with its two components:

```ts
app.post('/readings', express.json(), async (req, res, next) => {
  try {
    const user = currentUser(req);
    const { systolic, diastolic, measuredAt } = req.body;
    if (![systolic, diastolic].every((v) => Number.isFinite(v) && v > 20 && v < 300)) {
      throw new HttpError(400, 'systolic and diastolic must be numbers in mmHg');
    }
    const obs = await anpheros.observations.create(user.patientId, {
      code: '85354-9',
      category: 'vital-signs',
      effective_at: measuredAt ?? new Date().toISOString(),
      components: [
        { code: '8480-6', value: systolic, unit: 'mm[Hg]' },
        { code: '8462-4', value: diastolic, unit: 'mm[Hg]' },
      ],
      author_type: 'patient',
    }, { source_system: 'bp-app' });
    res.status(201).json({ id: obs.id, measuredAt: obs.effective_at });
  } catch (e) { next(e); }
});
```

What Anpheros stores and returns:

```json
{"id": "78188270-e65e-49bd-89d0-e776226e9614", "patient": "27296656-…", "code": "85354-9", "system": "http://loinc.org",
 "display": "Blood pressure panel", "category": "vital-signs", "effective_at": "2026-10-10T08:30:00Z",
 "components": [{"code": "8480-6", "system": "http://loinc.org", "value": 138.0, "unit": "mm[Hg]"},
                {"code": "8462-4", "system": "http://loinc.org", "value": 88.0, "unit": "mm[Hg]"}],
 "status": "final", "updated_at": "2026-10-12T20:16:51.153Z", "fhir": "Observation/78188270-…"}
```

The SDK sends an `Idempotency-Key` with every create, so a retry after a network error does not store the reading twice. `author_type: 'patient'` and `source_system` go into the reading's provenance: a doctor, another app or an AI model can later tell that the person measured this at home with your app.

## Step 4: read readings back

```ts
app.get('/readings', async (req, res, next) => {
  try {
    const user = currentUser(req);
    const page = await anpheros.observations.list(user.patientId, {
      code: '85354-9',
      from: typeof req.query.from === 'string' ? req.query.from : undefined,
      limit: 50,
    });
    res.json({
      readings: page.data.map((o) => ({
        id: o.id,
        measuredAt: o.effective_at,
        systolic: o.components.find((c) => c.code === '8480-6')?.value,
        diastolic: o.components.find((c) => c.code === '8462-4')?.value,
      })),
      next: page.next_cursor,
    });
  } catch (e) { next(e); }
});
```

Your API returns the shape your app wants. The stored data stays standard. Because the user id is checked by `currentUser`, one user can never ask for another user's patient id.

## Step 5: errors

Anpheros errors arrive as `AnpherosError` with `status`, `type`, `message`, `field` and a `requestId`. Pass validation errors to the app and hide the rest:

```ts
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
  if (err instanceof HttpError) return res.status(err.status).json({ error: err.message });
  if (err instanceof AnpherosError && err.status === 400) {
    return res.status(400).json({ error: err.message, field: err.field });
  }
  console.error(err instanceof AnpherosError ? `anpheros ${err.status} ${err.type} request=${err.requestId}` : err);
  res.status(502).json({ error: 'medical record service unavailable, try again' });
});
```

For example, a reading without a date is refused by Anpheros with `{"error": {"type": "invalid", "message": "Field required (at effective_at)", "field": "effective_at"}}`. Rate limits (`429`) and server errors are retried by the SDK before you see them. Log the `requestId` and quote it if you contact support. The full list is in [Errors](https://developers.anpheros.com/guides/errors).

## Step 6: hear about new data with a webhook

Readings can also arrive from elsewhere: a clinic writing to the same record, a lab report, another app the person connected. Register an endpoint once:

```bash
curl -X POST https://platform.anpheros.com/v1/webhooks -H "Authorization: Bearer $ANPHEROS_KEY" \
     -H "Content-Type: application/json" \
     -d '{"url": "https://bp.example/webhooks/anpheros", "events": ["resource.created", "consent.revoked"]}'
```

The response contains a `secret` (`whsec_…`), shown once. Verify every delivery against the raw body before trusting it:

```ts
app.post('/webhooks/anpheros', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = await verifyWebhookSignature({
    secret: process.env.ANPHEROS_WEBHOOK_SECRET!,
    header: req.header('anpheros-signature'),
    body: req.body as Buffer,
  });
  if (!ok) return res.status(400).end();
  const event = JSON.parse((req.body as Buffer).toString('utf8'));
  // Events carry ids and versions, not clinical data: fetch what you need, then update caches or notify.
  console.log(event.type, event.data);
  res.status(204).end();
});

app.listen(3000, () => console.log('bp-api on http://localhost:3000'));
```

Event types are `resource.created`, `resource.updated`, `resource.deleted`, `consent.granted`, `consent.revoked`, `document.finalized` and `ping`. Deliveries are signed, retried and can be replayed ([Webhooks](https://developers.anpheros.com/guides/webhooks)).

## Run it

```bash
npx tsx server.ts
curl -X POST localhost:3000/signup -H 'content-type: application/json' \
     -d '{"email":"ioana@example.com","given":"Ioana","family":"Marin","birthDate":"1979-04-12"}'
# {"userId":"…"}
curl -X POST localhost:3000/readings -H 'content-type: application/json' -H 'x-user-id: <userId>' \
     -d '{"systolic":138,"diastolic":88,"measuredAt":"2026-10-10T08:30:00Z"}'
curl localhost:3000/readings -H 'x-user-id: <userId>'
```

Replace the `x-user-id` stand-in with your real authentication before anything leaves your machine.

## Going further

- **People who already have a record.** An API key reaches the patients your project created. To connect to a person's existing record, collected by other apps, clinics or labs, use OAuth and their consent: [How to implement patient consent with SMART on FHIR](https://developers.anpheros.com/guides/smart-on-fhir-backend).
- **Strict FHIR when you need it.** `anpheros.fhir.search`, `create`, `transaction`, `everything` and `summary` give you the [FHIR R4 API](https://developers.anpheros.com/guides/store-medical-records-fhir) with the same ids.
- **AI features.** `anpheros.context.build` prepares a source-labelled context for a model: [How to build an AI healthcare app with a FHIR backend](https://developers.anpheros.com/guides/build-ai-healthcare-app).
- **Production.** Production keys (`sk_live_…`) are issued to verified organisations with a signed DPA; see [Pricing](https://developers.anpheros.com/guides/pricing) and [Limits and rate limits](https://developers.anpheros.com/guides/limits).

## Frequently asked questions

### Do I need my own database for medical data?
Not for the medical data itself. Your database keeps users, sessions and product settings, plus the patient id for each user. Observations, conditions, medications, documents, their history and who wrote them are stored in the FHIR record.

### Can the mobile app call Anpheros directly?
With an API key, no: the key reaches every patient of your project and must stay on your server. A mobile app can call Anpheros directly with an OAuth token that the person granted for their own record, as in [How to build a healthcare app with Flutter](https://developers.anpheros.com/guides/healthcare-app-flutter).

### Which framework does this work with?
Any. The SDK only needs `fetch`, so it runs in Express, Fastify, Hono, NestJS, Next.js route handlers, Deno or Bun.

## Related

- [Healthcare API](https://developers.anpheros.com/guides/healthcare-api): what to look for in a healthcare API, and the Anpheros API at a glance
- [Medical data API](https://developers.anpheros.com/guides/medical-data-api): where each kind of medical data goes
- [How to store medical records using FHIR R4](https://developers.anpheros.com/guides/store-medical-records-fhir)
- [Webhooks](https://developers.anpheros.com/guides/webhooks) and [Errors](https://developers.anpheros.com/guides/errors)
- [Medical app backend and database](https://developers.anpheros.com/guides/medical-app-backend)
