---
{
  "title": "@atomic-ehr/codegen: Perfiles US Core en TypeScript",
  "description": "Genere clases de perfil tipadas a partir del US Core IG con @atomic-ehr/codegen. Construya Patients y observaciones de TA tipadas con fábricas tipadas, extensiones y slices tipados, validación con conocimiento de perfiles, type guards y bundles tipados.",
  "date": "2026-05-08",
  "author": "Aleksandr Penskoi",
  "reading-time": "8 minutes",
  "tags": [
    "FHIR Tools",
    "FHIR Standard",
    "Code Generation",
    "TypeScript",
    "Aidbox"
  ]
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---
Construir recursos US Core a mano es tedioso. Se estampa `meta.profile`, se buscan códigos LOINC, se escribe a mano la extensión anidada `us-core-race` — cada campo es un error tipográfico esperando ocurrir, cada perfil tiene su propia versión de la misma ceremonia.

[`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) hace desaparecer ese código repetitivo. Apúntelo al [US Core IG](https://www.hl7.org/fhir/us/core/) y obtendrá una clase TypeScript por perfil, con accesores tipados para valores fijos, extensiones y slices, más un `validate()` que conoce lo que el perfil requiere.

Este tutorial recorre ese flujo completo sobre dos perfiles US Core: [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) y [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html).

## Qué Va a Construir

Un conversor de CSV a FHIR, construido paso a paso:

1. generar clases de perfil para [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) y [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html) a partir de `hl7.fhir.us.core@8.0.1`,
2. convertir cada fila en un US Core Patient — setters de extensión tipados y `apply()`,
3. convertir cada fila en un US Core Blood Pressure — slices tipados, LOINC fijo y `validate()`,
4. empaquetarlos como un Bundle,
5. leer el bundle de vuelta con getters tipados para calcular una TA media,
6. enviar el bundle a un servidor Aidbox local.

## Requisitos Previos

- **Node.js 20+** (o Bun) — se importa `@atomic-ehr/codegen` como librería desde un script TypeScript y se ejecuta con `tsx` o `bun`. La salida generada es TypeScript puro sin dependencias npm en tiempo de ejecución.
- **TypeScript 5+**
- Familiaridad básica con FHIR y US Core (basta con saber qué significan «perfil» y «slice»)

## Paso 1 — Generar las Clases de Perfil

Configure un proyecto nuevo:

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

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

Dos aspectos importantes:

- **`generateProfile: true`** — emite una clase envolvente por perfil con accesores tipados para extensiones, slices y valores fijos. Sin él, solo se generan los tipos R4 base.
- **`treeShake: { ... }`** — solo se generan los canonicals listados y sus dependencias transitivas (~50 archivos en lugar de 250+).

Ejecútelo. `prettyReport(report)` imprime un resumen agrupado para que vea lo que se emitió sin tener que explorar el directorio de salida:

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

La estructura en disco tiene este aspecto:

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

El código completo del tutorial vive en [`Aidbox/examples`](https://github.com/Aidbox/examples/tree/main/developer-experience/atomic-ehr-codegen-typescript-us-core-profiles) — `generate.ts`, `load.ts`, `avg.ts`, el CSV y el directorio `fhir-types/` ya confirmado para que pueda explorar el código generado sin ejecutar el generador. Para una exploración más amplia de la API de perfiles, el repositorio de codegen también tiene un [ejemplo de prueba `typescript-r4-us-core`](https://github.com/atomic-ehr/codegen/tree/main/examples/typescript-r4-us-core).

## Paso 2 — Fila a US Core Patient

La entrada es `patients.csv` — datos demográficos básicos más una lectura de TA por paciente. La raza usa los [códigos de categoría OMB](https://www.hl7.org/fhir/us/core/ValueSet-omb-race-category.html) que espera 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 analizador trivial entrega cada fila como cadenas de texto simples; todo el estrechamiento de tipos y el análisis numérico ocurre más tarde, en el momento en que se pasan los valores a los setters de perfil tipados.

<details>
<summary>`parseCsv(path: string): Row[]` en `load.ts` — código repetitivo, haga clic para expandir</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>

Esa es la mitad aburrida. La mitad interesante es convertir cada `Row` en un US Core Patient — el perfil añade un puñado de extensiones y eleva `identifier` y `name` a obligatorios. La clase generada tiene setters tipados para todos ellos:

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

Dos fases:

1. **Construir el `Patient` plano** — los campos requeridos por el perfil (`identifier`, `name`) y los must-support (`gender`, `birthDate`) como un recurso R4 tipado.
2. **Luego `USCorePatientProfile.apply(basePatient)`** estampa `meta.profile` y devuelve una instancia de perfil con accesores tipados para las extensiones US Core.

Dos notas sobre lo que hace la API de perfil por usted:

- **Tres formas de setter de extensión.** `setRace({ ombCategory, text })` acepta entrada plana y genera el mecanismo de `extension[]` anidado. También acepta una instancia de perfil tipada o una extensión FHIR raw para paso directo.
- **Sin setters para los campos base must-support.** `gender`, `birthDate` y `address` no son perfilados más allá por US Core, por lo que la clase de perfil no emite envoltorios estilo `.setGender()` — rellénelos como campos normales de Patient. `validate()` aún avisa si falta un campo must-support.

## Paso 3 — Fila a US Core Blood Pressure

El perfil BP es donde el codegen realmente justifica su existencia. El perfil US Core Blood Pressure:

- fija `code` a LOINC 85354-9 («Blood pressure panel»),
- fija un slice de categoría `vital-signs`,
- define los slices `component[systolic]` y `component[diastolic]` con discriminadores LOINC específicos (8480-6 y 8462-4),
- requiere un `effectiveDateTime` o `effectivePeriod`,
- requiere `valueQuantity` dentro de cada slice.

Escribir todo eso a mano por fila es exactamente el tipo de tarea que el codegen elimina. La clase generada lo reduce a tres setters:

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

Lo que ocurre internamente:

- **`create()` hace la ceremonia.** Estampa `meta.profile`, rellena el `code` fijo (LOINC 85354-9), añade el slice de categoría vital-signs y agrega stubs vacíos de `component[systolic]` / `component[diastolic]` con los códigos discriminadores ya establecidos.
- **`setSystolic({ value, unit, ... })` rellena el `valueQuantity`** dentro del slice sistólico. El campo `code` discriminador en ese componente ya está ahí desde `create()` — usted solo proporciona la lectura.
- **`validate()` devuelve `{ errors, warnings }`.** Los errores bloquean (campos requeridos, campos excluidos, variantes de elección no permitidas, cardinalidad de slice). Los avisos señalan preocupaciones must-support. Una fila mal formada falla rápido con el MRN — no lo descubre en el momento del POST.

Usted no escribió los códigos discriminadores. No tuvo que recordar `85354-9`. Los únicos códigos en su fuente son los que el perfil no dicta — y para la TA, no hay ninguno.

## Paso 4 — Ensamblar el Bundle

Cada fila produce un Patient y una Observation de TA vinculados por el marcador `urn:uuid` del Patient. Empaquételos como entradas de transacción:

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

Ejecute el cargador completo:

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

Vale la pena destacar:

- **`Bundle<T>` se propaga.** Como `Bundle` y `BundleEntry` son genéricos sobre el recurso contenido (con `Resource` como valor por defecto), `Bundle<Patient | Observation>` estrecha `entry[].resource` a esa unión. Esa es la mitad a nivel de tipos; en el Paso 5 añadiremos estrechamiento en tiempo de ejecución con conocimiento de perfil usando `is()`.
- **Referencias mediante `urn:uuid`.** El `fullUrl` del paciente es un UUID; el `subject.reference` de la observación apunta al mismo UUID. `Reference.reference` está tipado como una unión que cubre todas las formas de referencia literal FHIR — `Patient/${id}`, absoluta `http://...`, `urn:uuid:...`, `urn:oid:...` y `#fragment` — por lo que el marcador encaja sin necesidad de un cast. Al confirmar la transacción, el servidor resuelve ambos UUID a IDs de recurso reales de forma atómica.

## Paso 5 — Lectura: TA Media del Bundle

Escribir es la mitad de la historia. Lea `bundle.json` de vuelta y calcule la TA media sistólica/diastólica para ejercitar la API de lectura en `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)
```

Tres cosas que hace el perfil aquí:

- **`is()` es un type guard.** Comprueba `resourceType` y `meta.profile.includes(canonicalUrl)`; úselo como predicado de `.filter()` en cualquier colección. No construye nada, no valida nada.
- **`from(obs)` valida los supervivientes.** Una vez que `is()` ha estrechado la entrada, `from()` ejecuta la comprobación estructural (campos requeridos, cardinalidad de slice) y lanza una excepción si un recurso que *declara* el perfil está mal formado — de modo que un bundle roto falla en el momento de la lectura, no en el siguiente acceso al campo.
- **`getSystolic()` / `getDiastolic()` devuelven slices planos.** Sin necesidad de recorrer `component[].code.coding[].code` para hacer coincidir los códigos LOINC. El perfil ya sabe qué slice es cuál.

Ese es el ciclo completo: CSV → perfiles tipados → Bundle validado → lectura de vuelta tipada con getters con conocimiento de perfil. Las mismas pocas líneas procesarían TAs obtenidas de un servidor FHIR, cargadas desde un archivo o recibidas en una Subscription — el perfil tipado es la forma común, independientemente de la fuente.

## Paso 6 — Enviar el Bundle a un Servidor FHIR

El pipeline tipado es solo la mitad de la historia. Para ver realmente la confirmación de la transacción — IDs de paciente asignados, referencias `urn:uuid` reescritas, recursos almacenados y buscables — necesita un servidor FHIR. Ponga en marcha y ejecute [Aidbox](https://www.health-samurai.io/aidbox):

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

Abra <http://localhost:8080> en su navegador para obtener una licencia gratuita de desarrollador y, a continuación, verifique que el endpoint FHIR está activo:

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

Debería ver un `CapabilityStatement` JSON.

Envíe el `bundle.json` que acaba de crear:

```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 devuelve un bundle `transaction-response` — una entrada por entrada de entrada, cada una con `201 Created` y una `location` que apunta al recurso almacenado:

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

Consulte una observación de vuelta y observe su `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..."
```

Sin `urn:uuid` — Aidbox reescribió los marcadores de forma atómica al confirmar.

## Qué Hacer a Continuación

- **Más de la API de perfiles.** Otras fábricas, getters y formas de slice/extensión se ejercitan en los tests de ejemplo del 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).
- **Mezcle perfiles de múltiples paquetes.** `APIBuilder.fromPackage()` puede encadenarse — US Core junto con su IG personalizado, o junto con una base regional. Consulte los ejemplos [`on-the-fly`](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly) para [KBV alemán](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly/kbv-r4) y [perfiles base noruegos](https://github.com/atomic-ehr/codegen/tree/main/examples/on-the-fly/norge-r4).
- **Parchee paquetes defectuosos al vuelo.** Los IGs del mundo real se distribuyen con defectos (canonicals con errores tipográficos, bindings faltantes). `preprocessPackage` le permite corregirlos en el momento de la carga sin bifurcar el paquete — vea [`on-the-fly/ccda`'s `generate.ts`](https://github.com/atomic-ehr/codegen/blob/main/examples/on-the-fly/ccda/generate.ts) reparando un canonical CDA con error tipográfico.

## Conclusión

El generador emite tanto los tipos R4 base como una fina capa de clases de perfil encima — sin DSL en tiempo de ejecución, sin ORM, sin framework. `toResource()` siempre le da un recurso FHIR plano que puede enviar a cualquier servidor.

`@atomic-ehr/codegen` tiene licencia MIT; los issues y los PRs son bienvenidos.

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