Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card
Como criar uma API de saúde com TypeScript
Tutorial: uma API Express em TypeScript que cria registos de pacientes, armazena leituras de pressão arterial como observações FHIR R4, lê-as de volta, mapeia erros e verifica webhooks — sem a sua própria base de dados médica.
Este tutorial constrói a API de backend de uma aplicação de monitorização de pressão arterial em TypeScript com Express: o registo cria o registo médico do utilizador, as leituras são armazenadas como observações HL7 FHIR R4 codificadas, a aplicação lê-as de volta e um endpoint de webhook é notificado sobre novos dados, sem uma base de dados médica própria. A sua API mantém o que é específico do seu produto (utilizadores, sessões, configurações). Os dados médicos, o seu histórico, consentimento e proveniência residem na Anpheros, a qual chama através do SDK TypeScript.
Arquitetura
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 ──────────────┘
A sua base de dados armazena uma coluna extra por utilizador: o seu ID de paciente Anpheros. Tudo o que é clínico passa pela API, pelo que é codificado, versionado e legível por qualquer sistema FHIR ou pelas outras aplicações do paciente a que ele consente. Backend e base de dados da aplicação médica explica a divisão em detalhe.
Antes de começar
- Node.js 18 ou mais recente.
- Uma chave de sandbox (
sk_test_…) de platform.anpheros.com/dashboard (Obter uma chave de sandbox). Mantenha-a apenas no servidor; nunca deve chegar à aplicação.
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_…
Passo 1: o cliente e a sua loja de utilizadores
// 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); }
}
Passo 2: Registo cria o registo médico
Quando alguém se regista, crie o seu paciente em Anpheros e coloque o seu próprio ID de utilizador em identifiers, para que o registo possa ser sempre encontrado do seu 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); }
});
A resposta de Anpheros é o novo paciente, com um id que pertence ao seu projeto e a sua referência 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"}
Passo 3: Armazenar uma leitura
A aplicação envia {systolic, diastolic, measuredAt}. A sua API valida-a e armazena uma observação de pressão arterial, LOINC 85354-9, com os seus dois 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); }
});
O que o Anpheros armazena e retorna:
{"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-…"}
O SDK envia um Idempotency-Key com cada criação, pelo que uma nova tentativa após um erro de rede não armazena a leitura duas vezes. author_type: 'patient' e source_system entram na proveniência da leitura: um médico, outra aplicação ou um modelo de IA podem mais tarde dizer que a pessoa mediu isto em casa com a sua aplicação.
Passo 4: Ler leituras de volta
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); }
});
A sua API devolve o formato que a sua aplicação pretende. Os dados armazenados mantêm-se standard. Como o ID do utilizador é verificado por currentUser, um utilizador nunca poderá solicitar o ID de paciente de outro utilizador.
Passo 5: Erros
Os erros do Anpheros chegam como AnpherosError com status, type, message, field e um requestId. Passe os erros de validação para a aplicação e oculte os restantes:
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 exemplo, uma leitura sem data é recusada pelo Anpheros com {"error": {"type": "invalid", "message": "Field required (at effective_at)", "field": "effective_at"}}. Limites de taxa (429) e erros de servidor são retentados pelo SDK antes de serem visíveis. Registe o requestId e cite-o se contactar o suporte. A lista completa encontra-se em Erros.
Passo 6: ser notificado de novos dados com um webhook
As leituras também podem chegar de outros locais: uma clínica a escrever no mesmo registo, um relatório de laboratório, outra aplicação que a pessoa conectou. Registe um endpoint uma 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"]}'
A resposta contém um secret (whsec_…), mostrado uma vez. Verifique cada entrega contra o corpo bruto antes de confiar nela:
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'));
Os tipos de evento são resource.created, resource.updated, resource.deleted, consent.granted, consent.revoked, document.finalized e ping. As entregas são assinadas, tentadas novamente e podem ser repetidas (Webhooks).
Executar
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>'
Substitua o marcador x-user-id pela sua autenticação real antes que qualquer coisa saia da sua máquina.
Aprofundar
- Pessoas que já têm um registo. Uma chave de API alcança os pacientes que o seu projeto criou. Para se ligar ao registo existente de uma pessoa, recolhido por outras aplicações, clínicas ou laboratórios, utilize OAuth e o seu consentimento: Como implementar o consentimento do paciente com SMART on FHIR.
- FHIR estrito quando precisar.
anpheros.fhir.search,create,transaction,everythingesummaryfornecem a API FHIR R4 com os mesmos IDs. - Funcionalidades de IA.
anpheros.context.buildprepara um contexto rotulado de origem para um modelo: Como criar uma aplicação de saúde com IA com um backend FHIR. - Produção. As chaves de produção (
sk_live_…) são emitidas para organizações verificadas com um DPA assinado; consulte Preços e Limites e limites de taxa.
Perguntas frequentes
Preciso da minha própria base de dados para dados médicos?
Não para os dados médicos em si. A sua base de dados guarda utilizadores, sessões e configurações do produto, mais o ID do paciente para cada utilizador. Observações, condições, medicamentos, documentos, o seu histórico e quem os escreveu são armazenados no registo FHIR.
A aplicação móvel pode chamar o Anpheros diretamente?
Com uma chave de API, não: a chave alcança todos os pacientes do seu projeto e deve permanecer no seu servidor. Uma aplicação móvel pode chamar o Anpheros diretamente com um token OAuth que a pessoa concedeu para o seu próprio registo, como em Como criar uma aplicação de saúde com Flutter.
Com que framework isto funciona?
Qualquer um. O SDK só precisa de fetch, pelo que funciona em Express, Fastify, Hono, NestJS, Next.js route handlers, Deno ou Bun.
Relacionado
- API de Saúde: o que procurar numa API de saúde e a API Anpheros em resumo
- API de dados médicos: para onde vai cada tipo de dado médico
- Como armazenar registos médicos usando FHIR R4
- Webhooks e Erros
- Backend e base de dados de aplicações médicas
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no card