---
{
  "title": "@atomic-ehr/codegen: US Core Profile in TypeScript",
  "description": "Generieren Sie typisierte Profilklassen aus dem US Core IG mit @atomic-ehr/codegen. Erstellen Sie konforme Patienten und Blutdruck-Observations mit typisierten Factories, typisierten Extensions und Slices, profilbewusster Validierung, Type Guards und typisierten Bundles.",
  "date": "2026-05-08",
  "author": "Aleksandr Penskoi",
  "reading-time": "8 minutes",
  "tags": [
    "FHIR Tools",
    "FHIR Standard",
    "Code Generation",
    "TypeScript",
    "Aidbox"
  ]
}
---
US Core-Ressourcen von Hand zu erstellen ist mühsam. Man stempelt `meta.profile`, schlägt LOINC-Codes nach, schreibt die verschachtelte Extension `us-core-race` manuell — jedes Feld ist ein Tippfehler, der darauf wartet, zu passieren, und jedes Profil ist seine eigene Version desselben Zeremoniells.

[`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) lässt diesen Boilerplate verschwinden. Richten Sie es auf das [US Core IG](https://www.hl7.org/fhir/us/core/) aus, und Sie erhalten eine TypeScript-Klasse pro Profil mit typisierten Accessors für feste Werte, Extensions und Slices sowie ein `validate()`, das weiß, was das Profil erfordert.

Dieses Tutorial führt Sie anhand zweier US Core-Profile Schritt für Schritt durch: [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) und [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html).

## Was Sie erstellen werden

Ein CSV-zu-FHIR-Konverter, Schritt für Schritt aufgebaut:

1. Generieren Sie Profilklassen für [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) und [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html) aus `hl7.fhir.us.core@8.0.1`,
2. wandeln Sie jede Zeile in einen US Core Patient um — typisierte Extension-Setter und `apply()`,
3. wandeln Sie jede Zeile in einen US Core Blood Pressure um — typisierte Slices, fester LOINC und `validate()`,
4. verpacken Sie alles als Bundle,
5. lesen Sie das Bundle mit typisierten Gettern zurück, um einen durchschnittlichen Blutdruck zu berechnen,
6. senden Sie das Bundle an einen lokalen Aidbox-Server.

## Voraussetzungen

- **Node.js 20+** (oder Bun) — Sie importieren `@atomic-ehr/codegen` als Bibliothek in einem TypeScript-Skript und führen es mit `tsx` oder `bun` aus. Die generierte Ausgabe ist reines TypeScript ohne Laufzeit-npm-Abhängigkeiten.
- **TypeScript 5+**
- Grundlegende Kenntnisse von FHIR und US Core (es genügt zu wissen, was „Profil" und „Slice" bedeuten)

## Schritt 1 — Profilklassen generieren

Legen Sie ein neues Projekt an:

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

Erstellen Sie `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();
```

Zwei Dinge sind hier wichtig:

- **`generateProfile: true`** — gibt pro Profil eine Wrapper-Klasse mit typisierten Accessors für Extensions, Slices und feste Werte aus. Ohne diese Option werden nur Basis-R4-Typen generiert.
- **`treeShake: { ... }`** — nur die aufgeführten Canonicals und ihre transitiven Abhängigkeiten werden generiert (~50 Dateien statt 250+).

Führen Sie es aus. `prettyReport(report)` gibt eine gruppierte Zusammenfassung aus, sodass Sie sehen, was erzeugt wurde, ohne das Ausgabeverzeichnis durchsuchen zu müssen:

```bash
$ npx tsx generate.ts
# Output trimmed for brevity
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
```

Die Verzeichnisstruktur auf der Festplatte sieht folgendermaßen aus:

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

Der vollständige Tutorial-Code befindet sich in [`Aidbox/examples`](https://github.com/Aidbox/examples/tree/main/developer-experience/atomic-ehr-codegen-typescript-us-core-profiles) — `generate.ts`, `load.ts`, `avg.ts`, die CSV-Datei und das eingecheckte `fhir-types/`-Verzeichnis, damit Sie den generierten Code durchstöbern können, ohne den Generator ausführen zu müssen. Für eine weitergehende Erkundung der Profil-API enthält das Codegen-Repository außerdem ein [`typescript-r4-us-core`-Testbeispiel](https://github.com/atomic-ehr/codegen/tree/main/examples/typescript-r4-us-core).

## Schritt 2 — Zeile zu einem US Core Patient

Die Eingabe ist `patients.csv` — grundlegende demografische Daten plus eine Blutdruckmessung pro Patient. Die Rasseangabe verwendet die [OMB-Kategoriecodes](https://www.hl7.org/fhir/us/core/ValueSet-omb-race-category.html), die US Core erwartet:

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

Ein einfacher Parser übergibt jede Zeile als reine Zeichenketten; alle Typeinschränkungen und numerischen Konvertierungen erfolgen später, an dem Punkt, an dem wir Werte an typisierte Profil-Setter übergeben.

<details>
<summary>`parseCsv(path: string): Row[]` in `load.ts` — Boilerplate, zum Erweitern klicken</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>

Das ist die langweilige Hälfte. Die interessante Hälfte ist die Umwandlung jeder `Row` in einen US Core Patient — das Profil ergänzt einige Extensions und macht `identifier` und `name` zu Pflichtfeldern. Die generierte Klasse verfügt über typisierte Setter für alle davon:

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

Zwei Phasen:

1. **Den einfachen `Patient` aufbauen** — profilpflichtige (`identifier`, `name`) und Must-Support-Felder (`gender`, `birthDate`) als typisierte R4-Ressource.
2. **Dann `USCorePatientProfile.apply(basePatient)`** stempelt `meta.profile` und gibt eine Profilinstanz mit typisierten Accessors für die US Core-Extensions zurück.

Zwei Anmerkungen zu dem, was die Profil-API für Sie erledigt:

- **Drei Formen des Extension-Setters.** `setRace({ ombCategory, text })` nimmt eine flache Eingabe entgegen und erzeugt die verschachtelte `extension[]`-Struktur. Es akzeptiert außerdem eine typisierte Profilinstanz oder eine rohe FHIR-Extension für die Durchleitung.
- **Keine Setter für Must-Support-Basisfelder.** `gender`, `birthDate` und `address` werden von US Core nicht weiter profiliert, sodass die Profilklasse keine `.setGender()`-ähnlichen Wrapper ausgibt — befüllen Sie diese wie normale Patient-Felder. `validate()` gibt dennoch eine Warnung aus, wenn ein Must-Support-Feld fehlt.

## Schritt 3 — Zeile zu einem US Core Blood Pressure

Das Blutdruckprofil ist der Bereich, in dem Codegen seinen wahren Wert zeigt. Das US Core Blood Pressure-Profil:

- fixiert `code` auf LOINC 85354-9 („Blood pressure panel"),
- fixiert einen `vital-signs`-Kategorie-Slice,
- definiert die Slices `component[systolic]` und `component[diastolic]` mit spezifischen LOINC-Diskriminatoren (8480-6 und 8462-4),
- erfordert ein `effectiveDateTime` oder `effectivePeriod`,
- erfordert `valueQuantity` innerhalb jedes Slices.

Dies für jede Zeile von Hand zu schreiben ist genau das, was Codegen überflüssig macht. Die generierte Klasse reduziert es auf drei Setter:

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

Was im Hintergrund passiert:

- **`create()` übernimmt das Zeremoniell.** Es stempelt `meta.profile`, füllt den festen `code` (LOINC 85354-9), hängt den Vital-Signs-Kategorie-Slice an und fügt leere `component[systolic]`- / `component[diastolic]`-Stubs mit bereits gesetzten Diskriminatorcodes hinzu.
- **`setSystolic({ value, unit, ... })` füllt die `valueQuantity`** innerhalb des systolischen Slices. Das Diskriminatorfeld `code` für diese Komponente ist bereits durch `create()` gesetzt — Sie geben lediglich den Messwert an.
- **`validate()` gibt `{ errors, warnings }` zurück.** Fehler blockieren die Verarbeitung (Pflichtfelder, ausgeschlossene Felder, unzulässige Choice-Varianten, Slice-Kardinalität). Warnungen weisen auf Must-Support-Probleme hin. Eine fehlerhafte Zeile schlägt sofort mit der MRN fehl — Sie bemerken das Problem nicht erst beim POST.

Sie haben die Diskriminatorcodes nicht eingetippt. Sie mussten sich `85354-9` nicht merken. Die einzigen Codes in Ihrem Quellcode sind die, die das Profil nicht vorschreibt — und beim Blutdruck gibt es keine davon.

## Schritt 4 — Das Bundle zusammenstellen

Jede Zeile erzeugt einen Patienten und eine Blutdruck-Observation, die durch den `urn:uuid`-Platzhalter des Patienten verknüpft sind. Verpacken Sie sie als Transaktions-Einträge:

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

Führen Sie den vollständigen Loader aus:

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

Erwähnenswert:

- **`Bundle<T>` wird weitergereicht.** Da `Bundle` und `BundleEntry` generisch über die enthaltene Ressource sind (Standard: `Resource`), schränkt `Bundle<Patient | Observation>` `entry[].resource` auf diese Union ein. Das ist die typsystemseitige Hälfte der Geschichte; in Schritt 5 ergänzen wir sie mit `is()` durch profilbewusstes Laufzeit-Narrowing.
- **Referenzen per `urn:uuid`.** Die `fullUrl` des Patienten ist eine UUID; das Feld `subject.reference` der Observation zeigt auf dieselbe UUID. `Reference.reference` ist als Union typisiert, die alle literalen FHIR-Referenzformen abdeckt — `Patient/${id}`, absolutes `http://...`, `urn:uuid:...`, `urn:oid:...` und `#fragment` — sodass der Platzhalter ohne Cast eingesetzt werden kann. Bei der Transaktionsübertragung löst der Server beide UUIDs atomar in echte Ressourcen-IDs auf.

## Schritt 5 — Zurücklesen: Durchschnittlicher Blutdruck aus dem Bundle

Das Schreiben ist nur die halbe Geschichte. Lesen Sie `bundle.json` zurück und berechnen Sie den durchschnittlichen systolischen/diastolischen Wert, um die Lese-API in `avg.ts` zu erproben:

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

Drei Dinge, die das Profil hier leistet:

- **`is()` ist ein Type Guard.** Prüft `resourceType` und `meta.profile.includes(canonicalUrl)`; verwenden Sie es als `.filter()`-Prädikat auf beliebigen Sammlungen. Es wird nichts konstruiert und nichts validiert.
- **`from(obs)` validiert die Überlebenden.** Sobald `is()` die Eingabe eingeschränkt hat, führt `from()` die Strukturprüfung durch (Pflichtfelder, Slice-Kardinalität) und wirft einen Fehler, wenn eine Ressource, die *das Profil beansprucht*, fehlerhaft ist — so schlägt ein beschädigtes Bundle beim Lesen fehl, nicht beim nächsten Feldzugriff.
- **`getSystolic()` / `getDiastolic()` geben flache Slices zurück.** Kein Durchlaufen von `component[].code.coding[].code`, um LOINC-Codes abzugleichen. Das Profil weiß bereits, welcher Slice welcher ist.

Das ist der vollständige Durchlauf: CSV → typisierte Profile → validiertes Bundle → typisiertes Zurücklesen mit profilbewussten Gettern. Dieselben wenigen Zeilen würden Blutdruckwerte verarbeiten, die von einem FHIR-Server abgerufen, aus einer Datei geladen oder über eine Subscription empfangen wurden — das typisierte Profil ist die gemeinsame Form, unabhängig von der Quelle.

## Schritt 6 — Das Bundle auf einem FHIR-Server speichern

Die typisierte Pipeline ist nur die halbe Geschichte. Um die Transaktion tatsächlich zu committen — Patienten-IDs zuzuweisen, `urn:uuid`-Referenzen umzuschreiben, Ressourcen zu speichern und durchsuchbar zu machen — benötigen Sie einen FHIR-Server. Starten Sie [Aidbox](https://www.health-samurai.io/aidbox):

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

Öffnen Sie <http://localhost:8080> in Ihrem Browser, um eine kostenlose Entwicklerlizenz zu erhalten, und überprüfen Sie dann, ob der FHIR-Endpunkt verfügbar ist:

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

Sie sollten ein JSON-`CapabilityStatement` sehen.

Senden Sie das soeben erstellte `bundle.json`:

```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 gibt ein `transaction-response`-Bundle zurück — einen Eintrag pro Eingabe, jeweils mit `201 Created` und einem `location`-Header, der auf die gespeicherte Ressource zeigt:

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

Rufen Sie eine Observation ab und betrachten Sie ihr `subject`-Feld:

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

Kein `urn:uuid` mehr — Aidbox hat die Platzhalter beim Commit atomar umgeschrieben.

## Wie es weitergeht

- **Mehr von der Profil-API.** Weitere Factories, Getter sowie Slice- und Extension-Formen werden in den Codegen-Beispieltests erprobt: [`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).
- **Profile aus mehreren Paketen kombinieren.** `APIBuilder.fromPackage()` kann verkettet werden — US Core zusammen mit Ihrem eigenen IG oder einem regionalen Basisprofil. Siehe die [`on-the-fly`](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly)-Beispiele für [German KBV](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly/kbv-r4) und [Norwegian base profiles](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly/norge-r4).
- **Fehlerhafte Pakete zur Laufzeit patchen.** Reale IGs werden mit Fehlern ausgeliefert (falsch geschriebene Canonicals, fehlende Bindings). `preprocessPackage` ermöglicht es, diese beim Laden zu korrigieren, ohne das Paket zu forken — siehe [`on-the-fly/ccda`'s `generate.ts`](https://github.com/atomic-ehr/codegen/blob/main/examples/on-the-fly/ccda/generate.ts), das einen falsch geschriebenen CDA-Canonical repariert.

## Fazit

Der Generator gibt sowohl die Basis-R4-Typen als auch eine schlanke Profilklassen-Schicht darüber aus — keine Laufzeit-DSL, kein ORM, kein Framework. `toResource()` liefert stets eine einfache FHIR-Ressource, die Sie an jeden beliebigen Server senden können.

`@atomic-ehr/codegen` ist MIT-lizenziert; Issues und Pull Requests sind willkommen.

[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 auf LinkedIn](https://www.linkedin.com/in/aleksandr-penskoi/)