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
- Node.js 18 o superior.
- Una clave de sandbox (
sk_test_…) de platform.anpheros.com/dashboard (Obtener una clave de sandbox). Guárdela solo en el servidor; nunca debe llegar a la aplicación.
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
- 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.
- FHIR estricto cuando lo necesite.
anpheros.fhir.search,create,transaction,everythingysummaryle proporcionan la API FHIR R4 con los mismos IDs. - Funciones de IA.
anpheros.context.buildprepara 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. - Producción. Las claves de producción (
sk_live_…) se emiten a organizaciones verificadas con un DPA firmado; consulte Precios y Límites y límites de tasa.
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
- API de Salud: qué buscar en una API de salud y la API de Anpheros de un vistazo
- API de datos médicos: dónde va cada tipo de dato médico
- Cómo almacenar registros médicos usando FHIR R4
- Webhooks y Errores
- Backend y base de datos de aplicaciones médicas
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card