AnpherosAnpheros PlatformDevelopers

Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card

Cómo crear una API sanitaria con TypeScript

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

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.

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 explica la división en detalle.

Antes de empezar

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

// 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:

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:

{"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:

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:

{"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

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:

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.

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:

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:

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

Ejecutarlo

npx tsx server.ts
curl -X POST localhost:3000/signup -H 'content-type: application/json' \
     -d '{"email":"[email protected]","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

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.

¿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

Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card

Product updates by email

New features, guides and the end of the beta. One email a month at most.

Send me occasional Anpheros Platform updates by email (new features, API changes, guides). I'll confirm by email and can unsubscribe in one click from any email. Privacy