AnpherosAnpheros PlatformDevelopers

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

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

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

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