Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card
Comment créer une API de santé avec TypeScript
Tutoriel : une API Express en TypeScript qui crée des dossiers patients, stocke des relevés de tension artérielle en tant qu'observations FHIR R4, les relit, mappe les erreurs et vérifie les webhooks — sans votre propre base de données médicale.
Ce tutoriel construit le backend de l'API d'une application de suivi de la tension artérielle en TypeScript avec Express : l'inscription crée le dossier médical de l'utilisateur, les relevés sont stockés sous forme d'observations HL7 FHIR R4 codées, l'application les relit, et un point de terminaison webhook est informé des nouvelles données, sans votre propre base de données médicale. Votre API conserve ce qui est spécifique à votre produit (utilisateurs, sessions, paramètres). Les données médicales, leur historique, leur consentement et leur provenance résident dans Anpheros, que vous appelez via le SDK TypeScript.
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 ──────────────┘
Votre base de données stocke une colonne supplémentaire par utilisateur : leur identifiant patient Anpheros. Tout ce qui est clinique passe par l'API, il est donc codé, versionné et lisible par tout système FHIR ou par les autres applications du patient auxquelles il consent. Backend et base de données de l'application médicale explique cette séparation en détail.
Avant de commencer
- Node.js 18 ou plus récent.
- Une clé sandbox (
sk_test_…) provenant de platform.anpheros.com/dashboard (Obtenir une clé sandbox). Conservez-la uniquement sur le serveur ; elle ne doit jamais atteindre l'application.
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_…
Étape 1 : le client et votre magasin d'utilisateurs
// 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); }
}
Étape 2 : L'inscription crée le dossier médical
Lorsqu'une personne s'inscrit, créez son patient dans Anpheros et placez votre propre identifiant utilisateur dans identifiers, afin que le dossier puisse toujours être retrouvé de votre côté :
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 réponse d'Anpheros est le nouveau patient, avec un id qui appartient à votre projet et sa référence 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"}
Étape 3 : stocker une mesure
L'application envoie {systolic, diastolic, measuredAt}. Votre API la valide et stocke une observation de pression artérielle, LOINC 85354-9, avec ses deux composantes :
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); }
});
Ce qu'Anpheros stocke et renvoie :
{"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-…"}
Le SDK envoie un Idempotency-Key à chaque création, ainsi une nouvelle tentative après une erreur réseau ne stocke pas la mesure en double. author_type: 'patient' et source_system sont intégrés à la provenance de la mesure : un médecin, une autre application ou un modèle d'IA pourra dire plus tard que la personne a effectué cette mesure à domicile avec votre application.
Étape 4 : Lire les relevés
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); }
});
Votre API renvoie la forme souhaitée par votre application. Les données stockées restent standard. Comme l'ID utilisateur est vérifié par currentUser, un utilisateur ne peut jamais demander l'ID patient d'un autre utilisateur.
Étape 5 : Erreurs
Les erreurs Anpheros arrivent avec un AnpherosError contenant un status, un type, un message, un field et un requestId. Transmettez les erreurs de validation à l'application et masquez les autres :
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' });
});
Par exemple, une lecture sans date est refusée par Anpheros avec {"error": {"type": "invalid", "message": "Field required (at effective_at)", "field": "effective_at"}}. Les limites de débit (429) et les erreurs serveur sont retentées par le SDK avant que vous ne les voyiez. Enregistrez le requestId et citez-le si vous contactez le support. La liste complète se trouve dans Erreurs.
Étape 6 : être informé des nouvelles données avec un webhook
Les lectures peuvent également provenir d'ailleurs : une clinique écrivant dans le même dossier, un rapport de laboratoire, une autre application à laquelle la personne s'est connectée. Enregistrez un point de terminaison une fois :
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 réponse contient un secret (whsec_…), affiché une fois. Vérifiez chaque livraison par rapport au corps brut avant de lui faire confiance :
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'));
Les types d'événements sont resource.created, resource.updated, resource.deleted, consent.granted, consent.revoked, document.finalized et ping. Les livraisons sont signées, retentées et peuvent être rejouées (Webhooks).
Exécution
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>'
Remplacez le substitut x-user-id par votre authentification réelle avant que quoi que ce soit ne quitte votre machine.
Aller plus loin
- Personnes ayant déjà un dossier. Une clé API permet d'accéder aux patients créés par votre projet. Pour vous connecter au dossier existant d'une personne, collecté par d'autres applications, cliniques ou laboratoires, utilisez OAuth et son consentement : Comment implémenter le consentement patient avec SMART on FHIR.
- FHIR strict lorsque vous en avez besoin.
anpheros.fhir.search,create,transaction,everythingetsummaryvous donnent l'API FHIR R4 avec les mêmes identifiants. - Fonctionnalités IA.
anpheros.context.buildprépare un contexte étiqueté par source pour un modèle : Comment construire une application IA de santé avec un backend FHIR. - Production. Les clés de production (
sk_live_…) sont délivrées aux organisations vérifiées avec un DPA signé ; consultez Tarifs et Limites et débits.
Questions fréquemment posées
Ai-je besoin de ma propre base de données pour les données médicales ?
Pas pour les données médicales elles-mêmes. Votre base de données conserve les utilisateurs, les sessions et les paramètres du produit, ainsi que l'identifiant du patient pour chaque utilisateur. Les observations, les conditions, les médicaments, les documents, leur historique et qui les a rédigés sont stockés dans le dossier FHIR.
L'application mobile peut-elle appeler Anpheros directement ?
Avec une clé API, non : la clé accède à tous les patients de votre projet et doit rester sur votre serveur. Une application mobile peut appeler Anpheros directement avec un jeton OAuth que la personne a accordé pour son propre dossier, comme dans Comment créer une application de santé avec Flutter.
Avec quel framework cela fonctionne-t-il ?
N'importe lequel. Le SDK n'a besoin que de fetch, il fonctionne donc dans Express, Fastify, Hono, NestJS, les gestionnaires de routes Next.js, Deno ou Bun.
Liens Connexes
- API de Santé : ce qu'il faut rechercher dans une API de santé, et l'API Anpheros en un coup d'œil
- API de données médicales : où vont les différents types de données médicales
- Comment stocker des dossiers médicaux en utilisant FHIR R4
- Webhooks et Erreurs
- Backend et base de données d'applications médicales
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card