# 如何使用 TypeScript 构建医疗保健 API

> 教程：一个使用 TypeScript 和 Express 构建的 API，可以创建患者记录，将血压读数存储为 FHIR R4 观测值，读回它们，映射错误并验证 Webhook — 无需您自己的医疗数据库。

Source: https://developers.anpheros.com/zh/guides/healthcare-api-typescript

**本教程将使用 TypeScript 和 Express 构建一个血压跟踪应用程序的后端 API：注册会创建用户的医疗记录，读数将作为编码的 HL7 FHIR R4 观测值存储，应用程序会读回它们，并且一个 Webhook 端点会接收新数据通知，而无需您自己的医疗数据库。** 您的 API 保留特定于您产品的内容（用户、会话、设置）。医疗数据、其历史记录、同意和来源信息存储在 Anpheros 中，您可以通过 [TypeScript SDK](https://developers.anpheros.com/zh/guides/sdks) 调用它。

## 架构

```
 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 患者 ID。所有临床数据都通过 API 进行处理，因此它可以被任何 FHIR 系统或患者授权的其他应用程序进行编码、版本化和读取。[医疗应用后端和数据库](https://developers.anpheros.com/zh/guides/medical-app-backend) 详细解释了这种拆分。

## 开始之前

- Node.js 18 或更高版本。
- 来自 [platform.anpheros.com/dashboard](https://platform.anpheros.com/dashboard/) 的沙盒密钥 (`sk_test_…`)（**获取沙盒密钥**）。请仅将其保存在服务器上；切勿泄露给应用程序。

```bash
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：客户端和您的用户存储

```ts
// 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 中创建他们的患者，并将您自己的用户 ID 放入 `identifiers`，以便您的系统可以始终找到该记录：

```ts
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 引用：

```json
{"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`，及其两个组成部分：

```ts
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 存储和返回的内容：

```json
{"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` 会被放入读数的来源信息中：医生、另一个应用或 AI 模型稍后可以得知该测量是用户在家中使用您的应用完成的。

## 步骤 4：读取读数

```ts
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 返回应用程序所需的形状。存储的数据保持标准。由于用户 ID 由 `currentUser` 检查，因此一个用户永远无法请求另一个用户的患者 ID。

## 第 5 步：错误

Anpheros 错误以 `AnpherosError` 的形式返回，包含 `status`、`type`、`message`、`field` 和一个 `requestId`。将验证错误传递给应用程序，并隐藏其他错误：

```ts
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"}}` 的形式拒绝。SDK 会重试速率限制（`429`）和服务器错误，然后再将它们暴露给您。请记录 `requestId`，并在联系支持人员时引用它。完整列表请参见 [错误](https://developers.anpheros.com/zh/guides/errors)。

## 步骤 6：通过 Webhook 获取新数据

数据也可以从其他地方到达：诊所写入同一记录、实验室报告、用户连接的另一个应用程序。只需注册一次端点：

```bash
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_…`)，仅显示一次。在信任任何交付之前，请对照原始正文进行验证：

```ts
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](https://developers.anpheros.com/zh/guides/webhooks)）。

## 运行它

```bash
npx tsx server.ts
curl -X POST localhost:3000/signup -H 'content-type: application/json' \
     -d '{"email":"ioana@example.com","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` 占位符替换为您真实的身份验证信息。

## 深入了解

- **已有记录的患者。** API 密钥可访问您的项目创建的患者记录。要连接到由其他应用程序、诊所或实验室收集的患者现有记录，请使用 OAuth 和患者的同意：[如何使用 SMART on FHIR 实现患者同意](https://developers.anpheros.com/zh/guides/smart-on-fhir-backend)。
- **需要时严格遵守 FHIR。** `anpheros.fhir.search`、`create`、`transaction`、`everything` 和 `summary` 可为您提供具有相同 ID 的 [FHIR R4 API](https://developers.anpheros.com/zh/guides/store-medical-records-fhir)。
- **AI 功能。** `anpheros.context.build` 为模型准备了带源标签的上下文：[如何使用 FHIR 后端构建 AI 医疗应用程序](https://developers.anpheros.com/zh/guides/build-ai-healthcare-app)。
- **生产环境。** 生产密钥 (`sk_live_…`) 将颁发给已签署 DPA 的已验证组织；请参阅 [定价](https://developers.anpheros.com/zh/guides/pricing) 和 [限制和速率限制](https://developers.anpheros.com/zh/guides/limits)。

## 常见问题解答

### 我需要为医疗数据维护自己的数据库吗？
医疗数据本身不需要。您的数据库用于存储用户、会话和产品设置，以及每个用户的患者 ID。观察记录、病情、用药、文档、它们的历史记录以及作者信息都存储在 FHIR 记录中。

### 移动应用可以直接调用 Anpheros 吗？
使用 API 密钥不行：该密钥可以访问您项目中的所有患者数据，并且必须保存在您的服务器上。移动应用可以通过用户为其自身记录授予的 OAuth 令牌直接调用 Anpheros，如 [如何使用 Flutter 构建医疗保健应用](https://developers.anpheros.com/guides/healthcare-app-flutter) 中所述。

### 这个 SDK 支持哪些框架？
任何框架都可以。该 SDK 只需要 `fetch`，因此可以在 Express、Fastify、Hono、NestJS、Next.js 路由处理器、Deno 或 Bun 中运行。

## 相关内容

- [医疗保健 API](https://developers.anpheros.com/zh/guides/healthcare-api)：医疗保健 API 的考量因素，以及 Anpheros API 概览
- [医疗数据 API](https://developers.anpheros.com/zh/guides/medical-data-api)：各类医疗数据去向
- [如何使用 FHIR R4 存储医疗记录](https://developers.anpheros.com/zh/guides/store-medical-records-fhir)
- [Webhooks](https://developers.anpheros.com/zh/guides/webhooks) 和 [错误](https://developers.anpheros.com/zh/guides/errors)
- [医疗应用后端和数据库](https://developers.anpheros.com/zh/guides/medical-app-backend)
