AnpherosAnpheros PlatformDevelopers

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

Как создать медицинский API на TypeScript

Руководство: API на Express в TypeScript, который создает записи пациентов, сохраняет показания артериального давления как FHIR R4 Observations, считывает их обратно, обрабатывает ошибки и проверяет вебхуки — без собственной базы данных медицинских данных.

В этом руководстве мы создадим бэкенд API для приложения отслеживания артериального давления на TypeScript с использованием Express: регистрация создает медицинскую запись пользователя, показания сохраняются как кодированные наблюдения HL7 FHIR R4, приложение считывает их обратно, а конечная точка веб-перехватчика получает уведомления о новых данных, при этом вам не потребуется собственная медицинская база данных. Ваш API хранит то, что специфично для вашего продукта (пользователи, сеансы, настройки). Медицинские данные, их история, согласие и происхождение находятся в Anpheros, к которому вы обращаетесь через TypeScript SDK.

Архитектура

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

Ваша база данных хранит один дополнительный столбец для каждого пользователя: идентификатор пациента Anpheros. Все клинические данные проходят через API, поэтому они кодируются, версионируются и доступны для чтения любой системой FHIR или другими приложениями пациента, на использование которых он дал согласие. Бэкенд и база данных медицинского приложения подробно объясняет разделение.

Перед началом работы

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

Шаг 1: клиент и ваше хранилище пользователей

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

Шаг 2: Регистрация создает медицинскую запись

Когда кто-то регистрируется, создайте пациента в Anpheros и поместите свой идентификатор пользователя в identifiers, чтобы запись всегда можно было найти с вашей стороны:

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

Ответом от Anpheros будет новый пациент с id, принадлежащим вашему проекту, и его 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"}

Шаг 3: сохранение показания

Приложение отправляет {systolic, diastolic, measuredAt}. Ваш API проверяет его и сохраняет одно наблюдение артериального давления, LOINC 85354-9, с двумя его компонентами:

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

Что Anpheros сохраняет и возвращает:

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

SDK отправляет Idempotency-Key при каждом создании, поэтому повторная попытка после сетевой ошибки не приведет к двойному сохранению показания. author_type: 'patient' и source_system включаются в происхождение показания: врач, другое приложение или модель ИИ смогут позже определить, что человек измерил это дома с помощью вашего приложения.

Шаг 4: Чтение показаний обратно

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

Ваш API возвращает данные в том виде, в котором они нужны вашему приложению. Хранимые данные остаются стандартными. Поскольку идентификатор пользователя проверяется currentUser, один пользователь никогда не сможет запросить идентификатор пациента другого пользователя.

Шаг 5: Ошибки

Ошибки Anpheros поступают в виде AnpherosError с status, type, message, field и requestId. Передавайте ошибки валидации приложению и скрывайте остальные:

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

Например, показание без даты отклоняется Anpheros с ошибкой {"error": {"type": "invalid", "message": "Field required (at effective_at)", "field": "effective_at"}}. Ограничения скорости (429) и ошибки сервера повторяются SDK до того, как вы их увидите. Запишите requestId и процитируйте его, если вы связываетесь со службой поддержки. Полный список находится в разделе Ошибки.

Шаг 6: Получение уведомлений о новых данных с помощью вебхука

Показания могут поступать и из других источников: от клиники, записывающей данные в ту же запись, от лабораторного отчета, от другого приложения, к которому подключился пользователь. Зарегистрируйте конечную точку один раз:

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"]}'

Ответ содержит secret (whsec_…), который отображается один раз. Проверяйте каждую доставку по необработанному телу перед тем, как доверять ей:

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

Типы событий: resource.created, resource.updated, resource.deleted, consent.granted, consent.revoked, document.finalized и ping. Доставки подписываются, повторяются и могут быть воспроизведены (Webhooks).

Запуск

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

Замените заполнитель x-user-id на ваши реальные учетные данные перед тем, как что-либо будет отправлено с вашего компьютера.

Дальнейшие шаги

Часто задаваемые вопросы

Нужна ли мне собственная база данных для медицинских данных?

Сами медицинские данные хранить не нужно. Ваша база данных предназначена для хранения информации о пользователях, сессиях, настройках продукта, а также идентификатора пациента для каждого пользователя. Наблюдения, состояния, лекарства, документы, их история и авторство хранятся в записи FHIR.

Может ли мобильное приложение напрямую вызывать Anpheros?

С ключом API — нет: ключ предоставляет доступ ко всем пациентам вашего проекта и должен храниться на вашем сервере. Мобильное приложение может напрямую вызывать Anpheros с помощью токена OAuth, который пользователь предоставил для своей собственной записи, как описано в Как создать медицинское приложение с помощью Flutter.

С каким фреймворком это работает?

С любым. SDK требует только fetch, поэтому он работает в Express, Fastify, Hono, NestJS, обработчиках маршрутов Next.js, Deno или Bun.

Связанные материалы

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