---
{
  "title": "@atomic-ehr/codegen : profils US Core en TypeScript",
  "description": "Générez des classes de profils typées à partir du IG US Core avec @atomic-ehr/codegen. Créez des patients conformes et des observations de PA avec des fabriques typées, des extensions et tranches typées, une validation tenant compte des profils, des gardes de type et des lots typés.",
  "date": "2026-05-08",
  "author": "Aleksandr Penskoi",
  "reading-time": "8 minutes",
  "tags": [
    "FHIR Tools",
    "FHIR Standard",
    "Code Generation",
    "TypeScript",
    "Aidbox"
  ]
}
---
Créer des ressources US Core à la main est fastidieux. On estampille `meta.profile`, on cherche les codes LOINC, on assemble à la main l'extension imbriquée `us-core-race` — chaque champ est une coquille en attente, chaque profil répète le même rituel.

[`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) fait disparaître ce code répétitif. Pointez-le vers le [US Core IG](https://www.hl7.org/fhir/us/core/) et vous obtenez une classe TypeScript par profil, avec des accesseurs typés pour les valeurs fixes, les extensions et les tranches, ainsi qu'un `validate()` qui connaît les exigences du profil.

Ce tutoriel parcourt le processus de bout en bout pour deux profils US Core : [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) et [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html).

## Ce que vous allez construire

Un convertisseur CSV vers FHIR, élaboré étape par étape :

1. générer des classes de profils pour [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) et [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html) à partir de `hl7.fhir.us.core@8.0.1`,
2. transformer chaque ligne en US Core Patient — accesseurs d'extensions typés et `apply()`,
3. transformer chaque ligne en US Core Blood Pressure — tranches typées, LOINC fixe et `validate()`,
4. les regrouper dans un Bundle,
5. relire le lot avec des accesseurs typés pour calculer une PA moyenne,
6. envoyer le lot à un serveur Aidbox local.

## Prérequis

- **Node.js 20+** (ou Bun) — vous importez `@atomic-ehr/codegen` en tant que bibliothèque depuis un script TypeScript et l'exécutez avec `tsx` ou `bun`. Le résultat généré est du TypeScript pur sans dépendances npm à l'exécution.
- **TypeScript 5+**
- Familiarité de base avec FHIR et US Core (savoir ce que signifient « profil » et « tranche » suffit)

## Étape 1 — Générer les classes de profils

Initialisez un nouveau projet :

```bash
mkdir ts-us-core-tutorial && cd ts-us-core-tutorial
npm init -y
npm install --save-dev @atomic-ehr/codegen tsx typescript
```

Créez `generate.ts` :

```typescript
import { APIBuilder, mkCodegenLogger, prettyReport } from "@atomic-ehr/codegen";

const main = async () => {
  const logger = mkCodegenLogger({
    suppressTags: ["#fieldTypeNotFound", "#duplicateSchema", "#duplicateCanonical", "#largeValueSet"],
  });

  const builder = new APIBuilder({ logger })
    .fromPackage("hl7.fhir.us.core", "8.0.1")
    .typeSchema({
      treeShake: {
        "hl7.fhir.us.core": {
          "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {},
          "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {},
        },
        "hl7.fhir.r4.core": {
          "http://hl7.org/fhir/StructureDefinition/Bundle": {},
        },
      },
    })
    .typescript({
      generateProfile: true,
    })
    .outputTo("./fhir-types")
    .cleanOutput(true);

  const report = await builder.generate();
  console.log(prettyReport(report));
  if (!report.success) process.exit(1);
};

main();
```

Deux éléments importants ici :

- **`generateProfile: true`** — émet une classe enveloppante par profil avec des accesseurs typés pour les extensions, les tranches et les valeurs fixes. Sans cette option, seuls les types R4 de base sont générés.
- **`treeShake: { ... }`** — seuls les canoniques listés et leurs dépendances transitives sont générés (~50 fichiers au lieu de 250+).

Exécutez-le. `prettyReport(report)` affiche un résumé groupé pour voir ce qui a été émis sans parcourir le répertoire de sortie :

```bash
$ npx tsx generate.ts
# Sortie abrégée pour plus de clarté
Generated files (12 kloc):
  - fhir-types/hl7-fhir-r4-core/Bundle.ts (69 loc)
  - fhir-types/hl7-fhir-r4-core/Observation.ts (112 loc)
  - fhir-types/hl7-fhir-r4-core/Patient.ts (80 loc)
  - fhir-types/hl7-fhir-us-core/profiles/Extension_USCoreRaceExtension.ts (265 loc)
  - fhir-types/hl7-fhir-us-core/profiles/Observation_USCoreBloodPressureProfile.ts (394 loc)
  - fhir-types/hl7-fhir-us-core/profiles/Patient_USCorePatientProfile.ts (253 loc)
Duration: 6978ms
Status: 🟩 Success
```

La structure sur disque ressemble à ceci :

```
fhir-types/
├── hl7-fhir-r4-core/                 # Base R4 types
│   ├── Bundle.ts
│   ├── Patient.ts
│   ├── Observation.ts
│   └── ...
├── hl7-fhir-us-core/
│   └── profiles/
│       ├── Patient_USCorePatientProfile.ts
│       ├── Observation_USCoreBloodPressureProfile.ts
│       ├── Extension_USCoreRaceExtension.ts
│       └── ...
└── profile-helpers.ts                 # Runtime helpers used by profile classes
```

Le code complet du tutoriel se trouve dans [`Aidbox/examples`](https://github.com/Aidbox/examples/tree/main/developer-experience/atomic-ehr-codegen-typescript-us-core-profiles) — `generate.ts`, `load.ts`, `avg.ts`, le CSV et le répertoire `fhir-types/` archivé pour que vous puissiez parcourir le code généré sans exécuter le générateur. Pour une exploration plus large de l'API de profils, le dépôt de codegen propose également un [exemple de test `typescript-r4-us-core`](https://github.com/atomic-ehr/codegen/tree/main/examples/typescript-r4-us-core).

## Étape 2 — Transformer une ligne en US Core Patient

L'entrée est `patients.csv` — données démographiques de base plus une lecture de PA par patient. La race utilise les [codes de catégorie OMB](https://www.hl7.org/fhir/us/core/ValueSet-omb-race-category.html) attendus par US Core :

```csv
mrn,family,given,birthDate,gender,raceCode,raceDisplay,effectiveDateTime,systolic,diastolic
MRN-001,Lovelace,Ada,1815-12-10,female,2106-3,White,2026-04-15,120,80
MRN-002,Turing,Alan,1912-06-23,male,2106-3,White,2026-04-15,118,76
MRN-003,Curie,Marie,1867-11-07,female,2106-3,White,2026-04-16,125,82
MRN-004,Carver,George,1864-01-01,male,2054-5,Black or African American,2026-04-16,135,88
MRN-005,Ochoa,Ellen,1958-05-10,female,2054-5,Black or African American,2026-04-17,128,84
```

Un analyseur rudimentaire transmet chaque ligne sous forme de chaînes simples ; tout le rétrécissement de type et l'analyse numérique s'effectuent ensuite, au moment où l'on passe les valeurs aux accesseurs de profil typés.

<details>
<summary>`parseCsv(path: string): Row[]` dans `load.ts` — code répétitif, cliquez pour développer</summary>

```typescript
import { readFileSync } from "node:fs";

type Row = {
  mrn: string;
  family: string;
  given: string;
  birthDate: string;
  gender: string;
  raceCode: string;
  raceDisplay: string;
  effectiveDateTime: string;
  systolic: string;
  diastolic: string;
};

const parseCsv = (path: string): Row[] => {
  const [header, ...lines] = readFileSync(path, "utf8").trim().split("\n");
  const cols = header!.split(",");
  return lines.map(line => {
    const values = line.split(",");
    return Object.fromEntries(cols.map((c, i) => [c, values[i]])) as Row;
  });
};
```

</details>

C'est la partie banale. La partie intéressante consiste à transformer chaque `Row` en US Core Patient — le profil ajoute quelques extensions et rend `identifier` et `name` obligatoires. La classe générée dispose d'accesseurs typés pour tous ces éléments :

```typescript
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
import { USCorePatientProfile } from "./fhir-types/hl7-fhir-us-core/profiles";

const rowToPatient = (row: Row): USCorePatientProfile => {
  const basePatient: Patient = {
    resourceType: "Patient",
    identifier: [{ system: "http://hospital.example.org/mrn", value: row.mrn }],
    name: [{ family: row.family, given: [row.given] }],
    gender: row.gender as Patient["gender"],
    birthDate: row.birthDate,
  };

  const patient = USCorePatientProfile.apply(basePatient);

  patient.setRace({
    ombCategory: { system: "urn:oid:2.16.840.1.113883.6.238", code: row.raceCode, display: row.raceDisplay },
    text: row.raceDisplay,
  });

  return patient;
};
```

Deux phases :

1. **Construire le `Patient` brut** — les champs requis par le profil (`identifier`, `name`) et à support obligatoire (`gender`, `birthDate`) comme ressource R4 typée.
2. **Puis `USCorePatientProfile.apply(basePatient)`** estampille `meta.profile` et retourne une instance de profil avec des accesseurs typés pour les extensions US Core.

Deux remarques sur ce que l'API de profil fait pour vous :

- **Trois formes d'accesseurs d'extension.** `setRace({ ombCategory, text })` prend une entrée à plat et génère la plomberie `extension[]` imbriquée. Elle accepte également une instance de profil typée ou une Extension FHIR brute pour un passage direct.
- **Pas d'accesseurs pour les champs de base à support obligatoire.** `gender`, `birthDate` et `address` ne sont pas davantage profilés par US Core, donc la classe de profil n'émet pas d'enveloppeurs de style `.setGender()` — renseignez-les comme des champs Patient normaux. `validate()` avertit tout de même si un champ à support obligatoire est absent.

## Étape 3 — Transformer une ligne en US Core Blood Pressure

Le profil PA est là où le codegen se distingue vraiment. Le profil US Core Blood Pressure :

- fixe `code` au LOINC 85354-9 (« panneau de pression artérielle »),
- fixe une tranche de catégorie `vital-signs`,
- définit des tranches `component[systolic]` et `component[diastolic]` avec des discriminateurs LOINC spécifiques (8480-6 et 8462-4),
- exige un `effectiveDateTime` ou `effectivePeriod`,
- exige un `valueQuantity` dans chaque tranche.

Assembler tout cela à la main par ligne est exactement le genre de travail qu'élimine le codegen. La classe générée réduit tout à trois accesseurs :

```typescript
import { USCoreBloodPressureProfile } from "./fhir-types/hl7-fhir-us-core/profiles";

const rowToBP = (row: Row, patientRef: `urn:uuid:${string}`): USCoreBloodPressureProfile => {
  const bp = USCoreBloodPressureProfile.create({
    status: "final",
    subject: { reference: patientRef },
  });

  bp
    .setEffectiveDateTime(row.effectiveDateTime)
    .setSystolic({ value: Number(row.systolic), unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" })
    .setDiastolic({ value: Number(row.diastolic), unit: "mmHg", system: "http://unitsofmeasure.org", code: "mm[Hg]" });

  const { errors } = bp.validate();
  if (errors.length) throw new Error(`${row.mrn}: ${errors.join("; ")}`);

  return bp;
};
```

Ce qui se passe en coulisses :

- **`create()` accomplit le rituel.** Il estampille `meta.profile`, remplit le `code` fixe (LOINC 85354-9), ajoute la tranche de catégorie vital-signs et insère des ébauches vides `component[systolic]` / `component[diastolic]` avec les codes discriminateurs déjà définis.
- **`setSystolic({ value, unit, ... })` remplit le `valueQuantity`** dans la tranche systolique. Le champ `code` discriminateur sur ce composant est déjà présent depuis `create()` — vous ne fournissez que la lecture.
- **`validate()` retourne `{ errors, warnings }`.** Les erreurs bloquent (champs requis, champs exclus, variantes de choix non autorisées, cardinalité des tranches). Les avertissements signalent les problèmes de support obligatoire. Une ligne malformée échoue rapidement avec le NIM — vous ne le découvrez pas au moment du POST.

Vous n'avez pas saisi les codes discriminateurs. Vous n'avez pas eu à mémoriser `85354-9`. Les seuls codes dans votre source sont ceux que le profil ne dicte pas — et pour la PA, il n'y en a aucun.

## Étape 4 — Assembler le Bundle

Chaque ligne produit un Patient et une Observation de PA liés par le `urn:uuid` de substitution du Patient. Regroupez-les comme entrées de transaction :

```typescript
import { writeFileSync } from "node:fs";
import { randomUUID } from "node:crypto";

import type { Bundle, BundleEntry } from "./fhir-types/hl7-fhir-r4-core/Bundle";
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation";

const rowToEntries = (row: Row): BundleEntry<Patient | Observation>[] => {
  const patientUrn: `urn:uuid:${string}` = `urn:uuid:${randomUUID()}`;
  const patient = rowToPatient(row);
  const bp = rowToBP(row, patientUrn);

  return [
    { fullUrl: patientUrn, resource: patient.toResource(), request: { method: "POST", url: "Patient" } },
    { fullUrl: `urn:uuid:${randomUUID()}`, resource: bp.toResource(), request: { method: "POST", url: "Observation" } },
  ];
};

const rows = parseCsv("./patients.csv");
console.log(`Loaded ${rows.length} rows`);

const bundle: Bundle<Patient | Observation> = {
  resourceType: "Bundle",
  type: "transaction",
  entry: rows.flatMap(rowToEntries),
};

writeFileSync("./bundle.json", JSON.stringify(bundle, null, 2));
console.log(`Wrote bundle with ${bundle.entry!.length} entries`);
```

Exécutez le chargeur complet :

```bash
$ npx tsx load.ts
# Loaded 5 rows
# Wrote bundle with 10 entries
```

Quelques points à noter :

- **`Bundle<T>` se propage.** Parce que `Bundle` et `BundleEntry` sont génériques sur la ressource contenue (avec `Resource` par défaut), `Bundle<Patient | Observation>` restreint `entry[].resource` à cette union. C'est la moitié typée de l'histoire ; à l'étape 5, nous ajouterons par-dessus un rétrécissement à l'exécution tenant compte des profils avec `is()`.
- **Références via `urn:uuid`.** Le `fullUrl` du patient est un UUID ; le `subject.reference` de l'observation pointe vers le même UUID. `Reference.reference` est typé comme une union couvrant toutes les formes de référence littérale FHIR — `Patient/${id}`, absolue `http://...`, `urn:uuid:...`, `urn:oid:...` et `#fragment` — donc le substitut s'insère sans transtypage. Lors de la validation de la transaction, le serveur résout les deux UUID en identifiants de ressources réels de manière atomique.

## Étape 5 — Relecture : PA moyenne à partir du Bundle

Écrire n'est que la moitié de l'histoire. Relisez `bundle.json` et calculez la PA systolique/diastolique moyenne pour exercer l'API de lecture dans `avg.ts` :

```typescript
import { readFileSync } from "node:fs";

import type { Bundle } from "./fhir-types/hl7-fhir-r4-core/Bundle";
import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation";
import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient";
import { USCoreBloodPressureProfile } from "./fhir-types/hl7-fhir-us-core/profiles";

const bundle: Bundle<Patient | Observation> = JSON.parse(readFileSync("./bundle.json", "utf8"));

const bps = (bundle.entry ?? [])
  .map(e => e.resource)
  .filter(USCoreBloodPressureProfile.is)
  .map(o => USCoreBloodPressureProfile.from(o));

const avg = (xs: number[]) => xs.reduce((s, x) => s + x, 0) / xs.length;

const systolic = bps.map(bp => bp.getSystolic()!.value!);
const diastolic = bps.map(bp => bp.getDiastolic()!.value!);

console.log(`Avg BP: ${avg(systolic).toFixed(1)}/${avg(diastolic).toFixed(1)} mmHg (n=${bps.length})`);
```

```bash
$ npx tsx avg.ts
Avg BP: 125.2/82.0 mmHg (n=5)
```

Trois choses que fait le profil ici :

- **`is()` est une garde de type.** Vérifie `resourceType` et `meta.profile.includes(canonicalUrl)` ; utilisez-le comme prédicat `.filter()` sur n'importe quelle collection. Rien n'est construit, rien n'est validé.
- **`from(obs)` valide les survivants.** Une fois que `is()` a restreint l'entrée, `from()` effectue la vérification structurelle (champs requis, cardinalité des tranches) et lève une exception si une ressource qui *revendique* le profil est malformée — ainsi un lot défectueux échoue à la lecture, pas au prochain accès de champ.
- **`getSystolic()` / `getDiastolic()` retournent des tranches à plat.** Plus besoin de parcourir `component[].code.coding[].code` pour faire correspondre les codes LOINC. Le profil sait déjà quelle tranche est laquelle.

Voilà le cycle complet : CSV → profils typés → Bundle validé → relecture typée avec des accesseurs tenant compte des profils. Ces quelques lignes identiques traiteraient des PA récupérées depuis un serveur FHIR, chargées depuis un fichier ou reçues sur un Subscription — le profil typé est la forme commune, quelle que soit la source.

## Étape 6 — Déposer votre Bundle sur un serveur FHIR

Le pipeline typé n'est que la moitié de l'histoire. Pour voir effectivement la transaction validée — identifiants de patients attribués, références `urn:uuid` réécrites, ressources stockées et interrogeables — vous avez besoin d'un serveur FHIR. Démarrez et exécutez [Aidbox](https://www.health-samurai.io/aidbox) :

```bash
curl -JO https://aidbox.app/runme && docker compose up
```

Ouvrez <http://localhost:8080> dans votre navigateur pour obtenir une licence développeur gratuite, puis vérifiez que le point de terminaison FHIR est opérationnel :

```bash
curl -u "root:$(awk '/BOX_ROOT_CLIENT_SECRET:/{print $2}' docker-compose.yaml)" http://localhost:8080/fhir/metadata
```

Vous devriez voir un `CapabilityStatement` JSON.

Envoyez le `bundle.json` que vous venez de créer :

```bash
curl -u "root:$(awk '/BOX_ROOT_CLIENT_SECRET:/{print $2}' docker-compose.yaml)" -X POST \
  -H "Content-Type: application/fhir+json" \
  -d @bundle.json http://localhost:8080/fhir
```

Aidbox retourne un lot `transaction-response` — une entrée par entrée d'entrée, chacune avec un `201 Created` et un `location` pointant vers la ressource stockée :

```json
{
  "resourceType": "Bundle",
  "type": "transaction-response",
  "entry": [
    { "response": { "status": "201 Created", "location": "Patient/<id>/_history/1" } },
    { "response": { "status": "201 Created", "location": "Observation/<id>/_history/1" } },
    ...
  ]
}
```

Interrogez une observation et examinez son `subject` :

```bash
curl -u "root:$(awk '/BOX_ROOT_CLIENT_SECRET:/{print $2}' docker-compose.yaml)" \
  "http://localhost:8080/fhir/Observation?code=http://loinc.org|85354-9" \
  | jq '.entry[].resource.subject.reference'
# "Patient/01J..."
# "Patient/01J..."
# "Patient/01J..."
```

Plus de `urn:uuid` — Aidbox a réécrit les substituts de manière atomique lors de la validation.

## Pour aller plus loin

- **Davantage d'API de profil.** D'autres fabriques, accesseurs et formes de tranches/extensions sont exercés dans les tests d'exemple du codegen : [`profile-us-core-patient.test.ts`](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-r4-us-core/profile-us-core-patient.test.ts), [`profile-us-core-bp.test.ts`](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-r4-us-core/profile-us-core-bp.test.ts), [`profile-us-core-bodyweight.test.ts`](https://github.com/atomic-ehr/codegen/blob/main/examples/typescript-r4-us-core/profile-us-core-bodyweight.test.ts).
- **Combiner des profils de plusieurs paquets.** `APIBuilder.fromPackage()` peut être enchaîné — US Core aux côtés de votre IG personnalisé, ou aux côtés d'une base régionale. Consultez les exemples [`on-the-fly`](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly) pour le [KBV allemand](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly/kbv-r4) et les [profils de base norvégiens](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly/norge-r4).
- **Corriger des paquets défectueux à la volée.** Les IG réels sont livrés avec des défauts (canoniques avec coquilles, liaisons manquantes). `preprocessPackage` vous permet de les corriger au chargement sans bifurquer le paquet — voir [`on-the-fly/ccda`'s `generate.ts`](https://github.com/atomic-ehr/codegen/blob/main/examples/on-the-fly/ccda/generate.ts) qui répare un canonique CDA contenant une coquille.

## Conclusion

Le générateur émet à la fois les types R4 de base et une couche légère de classes de profils par-dessus — pas de DSL à l'exécution, pas d'ORM, pas de cadriciel. `toResource()` vous donne toujours une ressource FHIR brute que vous pouvez envoyer à n'importe quel serveur.

`@atomic-ehr/codegen` est sous licence MIT ; les signalements de problèmes et les demandes de tirage sont les bienvenus.

[GitHub](https://github.com/atomic-ehr/codegen) | [NPM](https://www.npmjs.com/package/@atomic-ehr/codegen) | [US Core IG](https://www.hl7.org/fhir/us/core/) | [Aleksandr Penskoi sur LinkedIn](https://www.linkedin.com/in/aleksandr-penskoi/)