AnpherosAnpheros PlatformDevelopers

Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card

Come creare un'API sanitaria con TypeScript

Tutorial: un'API Express in TypeScript che crea record paziente, memorizza letture di pressione sanguigna come osservazioni FHIR R4, le rilegge, mappa errori e verifica webhook — senza un tuo database medico.

Questo tutorial costruisce il backend API di un'app di monitoraggio della pressione sanguigna in TypeScript con Express: la registrazione crea il record medico dell'utente, le letture vengono memorizzate come osservazioni codificate HL7 FHIR R4, l'app le rilegge e un endpoint webhook viene informato sui nuovi dati, senza un database medico proprio. La tua API conserva ciò che è specifico del tuo prodotto (utenti, sessioni, impostazioni). I dati medici, la loro cronologia, il consenso e la provenienza vivono in Anpheros, che chiami tramite l'SDK TypeScript.

Architettura

 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 ──────────────┘

Il tuo database memorizza una colonna aggiuntiva per utente: il loro ID paziente Anpheros. Tutto ciò che è clinico passa attraverso l'API, quindi è codificato, versionato e leggibile da qualsiasi sistema FHIR o dalle altre app del paziente a cui acconsente. Backend e database delle app mediche spiega la suddivisione in dettaglio.

Prima di iniziare

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_…

Passaggio 1: il client e il tuo store utente

// 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); }
}

Passaggio 2: la registrazione crea la cartella clinica

Quando qualcuno si registra, crea il proprio paziente in Anpheros e inserisci il tuo user id in identifiers, in modo che la cartella possa essere sempre trovata dal tuo lato:

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); }
});

La risposta da Anpheros è il nuovo paziente, con un id che appartiene al tuo progetto e il suo riferimento FHIR:

{"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"}

Passaggio 3: memorizzare una lettura

L'app invia {systolic, diastolic, measuredAt}. La tua API la convalida e memorizza un'osservazione della pressione sanguigna, LOINC 85354-9, con i suoi due componenti:

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); }
});

Cosa memorizza e restituisce Anpheros:

{"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-…"}

L'SDK invia un Idempotency-Key con ogni creazione, in modo che un nuovo tentativo dopo un errore di rete non memorizzi la lettura due volte. author_type: 'patient' e source_system vengono inseriti nella provenienza della lettura: un medico, un'altra app o un modello AI possono in seguito indicare che la persona ha effettuato questa misurazione a casa con la tua app.

Passo 4: Lettura delle letture

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); }
});

La tua API restituisce la forma desiderata dalla tua app. I dati memorizzati rimangono standard. Poiché l'ID utente viene verificato da currentUser, un utente non può mai richiedere l'ID paziente di un altro utente.

Step 5: errori

Gli errori di Anpheros arrivano come AnpherosError con status, type, message, field e un requestId. Passa gli errori di validazione all'app e nascondi gli altri:

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' });
});

Ad esempio, una lettura senza data viene rifiutata da Anpheros con {"error": {"type": "invalid", "message": "Field required (at effective_at)", "field": "effective_at"}}. I rate limit (429) e gli errori del server vengono ritentati dall'SDK prima che tu li veda. Registra il requestId e citarlo se contatti il supporto. L'elenco completo è in Errori.

Passo 6: Ricevere notifiche su nuovi dati tramite webhook

Le letture possono arrivare anche da altre fonti: una clinica che scrive sullo stesso record, un referto di laboratorio, un'altra app a cui la persona si è connessa. Registra un endpoint una sola volta:

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"]}'

La risposta contiene un secret (whsec_…), mostrato una volta. Verifica ogni consegna rispetto al corpo grezzo prima di fidarti:

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'));

I tipi di evento sono resource.created, resource.updated, resource.deleted, consent.granted, consent.revoked, document.finalized e ping. Le consegne sono firmate, ritentate e possono essere riprodotte (Webhook).

Eseguilo

npx tsx server.ts
curl -X POST localhost:3000/signup -H 'content-type: application/json' \
     -d '{"email":"[email protected]","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>'

Sostituisci il segnaposto x-user-id con la tua autenticazione reale prima che qualcosa lasci la tua macchina.

Approfondimenti

Domande frequenti

Ho bisogno di un mio database per i dati medici?

Non per i dati medici in sé. Il tuo database conserva utenti, sessioni e impostazioni del prodotto, oltre all'ID paziente per ciascun utente. Osservazioni, condizioni, farmaci, documenti, la loro cronologia e chi li ha scritti sono archiviati nel record FHIR.

L'app mobile può chiamare Anpheros direttamente?

Con una chiave API, no: la chiave raggiunge ogni paziente del tuo progetto e deve rimanere sul tuo server. Un'app mobile può chiamare Anpheros direttamente con un token OAuth che la persona ha concesso per il proprio record, come in Come creare un'app sanitaria con Flutter.

Con quale framework funziona?

Qualsiasi. L'SDK necessita solo di fetch, quindi funziona in Express, Fastify, Hono, NestJS, gestori di route Next.js, Deno o Bun.

Correlati

Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card

Product updates by email

New features, guides and the end of the beta. One email a month at most.

Send me occasional Anpheros Platform updates by email (new features, API changes, guides). I'll confirm by email and can unsubscribe in one click from any email. Privacy