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
- Node.js 18 o versione successiva.
- Una chiave sandbox (
sk_test_…) da platform.anpheros.com/dashboard (Ottieni una chiave sandbox). Conservala solo sul server; non deve mai raggiungere l'app.
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
- Persone che hanno già un record. Una chiave API raggiunge i pazienti creati dal tuo progetto. Per connettersi al record esistente di una persona, raccolto da altre app, cliniche o laboratori, usa OAuth e il loro consenso: Come implementare il consenso del paziente con SMART on FHIR.
- FHIR rigoroso quando necessario.
anpheros.fhir.search,create,transaction,everythingesummaryti forniscono l'API FHIR R4 con gli stessi ID. - Funzionalità AI.
anpheros.context.buildprepara un contesto etichettato per sorgente per un modello: Come creare un'app sanitaria AI con un backend FHIR. - Produzione. Le chiavi di produzione (
sk_live_…) vengono rilasciate a organizzazioni verificate con un DPA firmato; consulta Prezzi e Limiti e rate limit.
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
- API Sanitaria: cosa cercare in un'API sanitaria e l'API Anpheros in sintesi
- API Dati Medici: dove vanno i dati medici di ogni tipo
- Come archiviare cartelle cliniche utilizzando FHIR R4
- Webhook e Errori
- Backend e database per app mediche
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card