# Comment construire 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 sous forme d'observations FHIR R4, les relit, mappe les erreurs et vérifie les webhooks — sans votre propre base de données médicale.

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

**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](https://developers.anpheros.com/fr/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 ──────────────┘
```

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](https://developers.anpheros.com/fr/guides/medical-app-backend) 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](https://platform.anpheros.com/dashboard/) (**Obtenir une clé sandbox**). Conservez-la uniquement sur le serveur ; elle ne doit jamais atteindre l'application.

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

## Étape 1 : le client et votre magasin d'utilisateurs

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

## É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é :

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

La réponse d'Anpheros est le nouveau patient, avec un `id` qui appartient à votre projet et sa référence FHIR :

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

## É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 :

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

Ce qu'Anpheros stocke et renvoie :

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

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

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

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 :

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

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](https://developers.anpheros.com/fr/guides/errors).

## É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 :

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

La réponse contient un `secret` (`whsec_…`), affiché une fois. Vérifiez chaque livraison par rapport au corps brut avant de lui faire confiance :

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

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](https://developers.anpheros.com/fr/guides/webhooks)).

## Exécution

```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>'
```

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](https://developers.anpheros.com/fr/guides/smart-on-fhir-backend).
- **FHIR strict lorsque vous en avez besoin.** `anpheros.fhir.search`, `create`, `transaction`, `everything` et `summary` vous donnent l'[API FHIR R4](https://developers.anpheros.com/fr/guides/store-medical-records-fhir) avec les mêmes identifiants.
- **Fonctionnalités IA.** `anpheros.context.build` prépare un contexte étiqueté par source pour un modèle : [Comment construire une application IA de santé avec un backend FHIR](https://developers.anpheros.com/fr/guides/build-ai-healthcare-app).
- **Production.** Les clés de production (`sk_live_…`) sont délivrées aux organisations vérifiées avec un DPA signé ; consultez [Tarifs](https://developers.anpheros.com/fr/guides/pricing) et [Limites et débits](https://developers.anpheros.com/fr/guides/limits).

## 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](https://developers.anpheros.com/fr/guides/healthcare-app-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é](https://developers.anpheros.com/fr/guides/healthcare-api) : 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](https://developers.anpheros.com/fr/guides/medical-data-api) : où vont les différents types de données médicales
- [Comment stocker des dossiers médicaux en utilisant FHIR R4](https://developers.anpheros.com/fr/guides/store-medical-records-fhir)
- [Webhooks](https://developers.anpheros.com/fr/guides/webhooks) et [Erreurs](https://developers.anpheros.com/fr/guides/errors)
- [Backend et base de données d'applications médicales](https://developers.anpheros.com/fr/guides/medical-app-backend)
