# Cómo crear una API de salud con TypeScript

> Tutorial: una API de Express en TypeScript que crea registros de pacientes, almacena lecturas de presión arterial como observaciones FHIR R4, las lee de nuevo, mapea errores y verifica webhooks, sin una base de datos médica propia.

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

**Este tutorial crea la API backend de una aplicación de seguimiento de presión arterial en TypeScript con Express: el registro crea el historial médico del usuario, las lecturas se almacenan como observaciones codificadas HL7 FHIR R4, la aplicación las lee de nuevo y un endpoint de webhook se entera de los nuevos datos, sin una base de datos médica propia.** Su API mantiene lo que es específico de su producto (usuarios, sesiones, configuraciones). Los datos médicos, su historial, consentimiento y procedencia viven en Anpheros, al que llama a través del [SDK de TypeScript](https://developers.anpheros.com/es/guides/sdks).

## Arquitectura

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

Tu base de datos almacena una columna adicional por usuario: su ID de paciente Anpheros. Todo lo clínico pasa por la API, por lo que está codificado, versionado y es legible por cualquier sistema FHIR o por otras aplicaciones del paciente a las que dé su consentimiento. [Backend y base de datos de la aplicación médica](https://developers.anpheros.com/es/guides/medical-app-backend) explica la división en detalle.

## Antes de empezar

- Node.js 18 o superior.
- Una clave de sandbox (`sk_test_…`) de [platform.anpheros.com/dashboard](https://platform.anpheros.com/dashboard/) (**Obtener una clave de sandbox**). Guárdela solo en el servidor; nunca debe llegar a la aplicación.

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

## Paso 1: el cliente y su almacén de usuarios

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

## Paso 2: El registro crea el historial médico

Cuando alguien se registra, crea su paciente en Anpheros y coloca su propio ID de usuario en `identifiers`, para que el registro siempre se pueda encontrar desde su lado:

```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 respuesta de Anpheros es el nuevo paciente, con un `id` que pertenece a su proyecto y su referencia 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"}
```

## Paso 3: Almacenar una lectura

La aplicación envía `{systolic, diastolic, measuredAt}`. Su API la valida y almacena una observación de presión arterial, LOINC `85354-9`, con sus dos componentes:

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

Lo que Anpheros almacena y devuelve:

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

El SDK envía un `Idempotency-Key` con cada creación, por lo que un reintento después de un error de red no almacena la lectura dos veces. `author_type: 'patient'` y `source_system` se incluyen en la procedencia de la lectura: un médico, otra aplicación o un modelo de IA pueden decir más tarde que la persona se midió esto en casa con su aplicación.

## Paso 4: Leer lecturas

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

Tu API devuelve la forma que tu aplicación desea. Los datos almacenados permanecen estándar. Dado que el ID de usuario es verificado por `currentUser`, un usuario nunca podrá solicitar el ID de paciente de otro usuario.

## Paso 5: Errores

Los errores de Anpheros llegan como `AnpherosError` con `status`, `type`, `message`, `field` y un `requestId`. Pase los errores de validación a la aplicación y oculte el resto:

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

Por ejemplo, una lectura sin fecha es rechazada por Anpheros con `{"error": {"type": "invalid", "message": "Field required (at effective_at)", "field": "effective_at"}}`. Los límites de tasa (`429`) y los errores del servidor son reintentados por el SDK antes de que usted los vea. Registre el `requestId` y cítelo si contacta con soporte. La lista completa se encuentra en [Errores](https://developers.anpheros.com/es/guides/errors).

## Paso 6: Recibir nuevos datos con un webhook

Las lecturas también pueden llegar desde otros lugares: una clínica que escribe en el mismo registro, un informe de laboratorio, otra aplicación que la persona conectó. Registre un endpoint una vez:

```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 respuesta contiene un `secret` (`whsec_…`), mostrado una vez. Verifique cada entrega contra el cuerpo sin procesar antes de confiar en ella:

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

Los tipos de eventos son `resource.created`, `resource.updated`, `resource.deleted`, `consent.granted`, `consent.revoked`, `document.finalized` y `ping`. Las entregas se firman, se reintentan y se pueden reproducir ([Webhooks](https://developers.anpheros.com/es/guides/webhooks)).

## Ejecutarlo

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

Reemplace el marcador de posición `x-user-id` con su autenticación real antes de que algo salga de su máquina.

## Profundizando

- **Personas que ya tienen un registro.** Una clave de API accede a los pacientes que su proyecto creó. Para conectarse al registro existente de una persona, recopilado por otras aplicaciones, clínicas o laboratorios, utilice OAuth y su consentimiento: [Cómo implementar el consentimiento del paciente con SMART on FHIR](https://developers.anpheros.com/es/guides/smart-on-fhir-backend).
- **FHIR estricto cuando lo necesite.** `anpheros.fhir.search`, `create`, `transaction`, `everything` y `summary` le proporcionan la [API FHIR R4](https://developers.anpheros.com/es/guides/store-medical-records-fhir) con los mismos IDs.
- **Funciones de IA.** `anpheros.context.build` prepara un contexto etiquetado por fuente para un modelo: [Cómo crear una aplicación de atención médica con IA con un backend FHIR](https://developers.anpheros.com/es/guides/build-ai-healthcare-app).
- **Producción.** Las claves de producción (`sk_live_…`) se emiten a organizaciones verificadas con un DPA firmado; consulte [Precios](https://developers.anpheros.com/es/guides/pricing) y [Límites y límites de tasa](https://developers.anpheros.com/es/guides/limits).

## Preguntas frecuentes

### ¿Necesito mi propia base de datos para los datos médicos?
No para los datos médicos en sí. Su base de datos almacena usuarios, sesiones y configuraciones del producto, además del ID del paciente para cada usuario. Las observaciones, condiciones, medicamentos, documentos, su historial y quién los escribió se almacenan en el registro FHIR.

### ¿Puede la aplicación móvil llamar directamente a Anpheros?
Con una clave API, no: la clave accede a todos los pacientes de su proyecto y debe permanecer en su servidor. Una aplicación móvil puede llamar directamente a Anpheros con un token OAuth que la persona otorgó para su propio registro, como se describe en [Cómo crear una aplicación de atención médica con Flutter](https://developers.anpheros.com/es/guides/healthcare-app-flutter).

### ¿Con qué framework funciona esto?
Cualquiera. El SDK solo necesita `fetch`, por lo que se ejecuta en Express, Fastify, Hono, NestJS, manejadores de rutas Next.js, Deno o Bun.

## Relacionado

- [API de Salud](https://developers.anpheros.com/es/guides/healthcare-api): qué buscar en una API de salud y la API de Anpheros de un vistazo
- [API de datos médicos](https://developers.anpheros.com/es/guides/medical-data-api): dónde va cada tipo de dato médico
- [Cómo almacenar registros médicos usando FHIR R4](https://developers.anpheros.com/es/guides/store-medical-records-fhir)
- [Webhooks](https://developers.anpheros.com/es/guides/webhooks) y [Errores](https://developers.anpheros.com/es/guides/errors)
- [Backend y base de datos de aplicaciones médicas](https://developers.anpheros.com/es/guides/medical-app-backend)
