# TypeScript でヘルスケア API を構築する方法

> チュートリアル：Express API を TypeScript で構築し、患者レコードの作成、血圧測定値の FHIR R4 Observation としての保存、読み出し、エラーのマッピング、Webhook の検証を行います。独自の医療データベースは不要です。

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

**このチュートリアルでは、Express を使用した TypeScript による血圧追跡アプリのバックエンド API を構築します。サインアップでユーザーの医療レコードが作成され、測定値はコード化された HL7 FHIR R4 Observation として保存され、アプリはそれらを読み出し、Webhook エンドポイントは新しいデータについて通知を受け取ります。独自の医療データベースは不要です。** API は、製品固有の情報（ユーザー、セッション、設定）を保持します。医療データ、その履歴、同意、および出所は Anpheros に保存され、[TypeScript SDK](https://developers.anpheros.com/ja/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 ──────────────┘
```

データベースには、ユーザーごとに1つの追加列（Anpheros 患者 ID）が格納されます。臨床データはすべてAPIを経由するため、コード化され、バージョン管理され、任意のFHIRシステムまたは患者が同意した他のアプリで読み取ることができます。[医療アプリのバックエンドとデータベース](https://developers.anpheros.com/ja/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 でその患者を作成し、あなたの側から常に記録を見つけられるように、`identifiers` にあなた自身のユーザー ID を設定してください。

```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` の血圧測定値を 2 つのコンポーネントとともに保存します。

```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 のエラーは、`status`、`type`、`message`、`field`、そして `requestId` を含む `AnpherosError` として返されます。バリデーションエラーはアプリケーションに渡し、それ以外は非表示にしてください。

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

例えば、日付のない測定値は `{"error": {"type": "invalid", "message": "Field required (at effective_at)", "field": "effective_at"}}` として Anpheros によって拒否されます。レート制限 (`429`) やサーバーエラーは、SDK によってリトライされてから表示されます。`requestId` をログに記録し、サポートに連絡する際にはそれを引用してください。完全なリストは [エラー](https://developers.anpheros.com/ja/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` です。配信は署名され、リトライされ、リプレイ可能です ([Webhook](https://developers.anpheros.com/ja/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/ja/guides/smart-on-fhir-backend)。
- **必要な場合の厳格な FHIR。** `anpheros.fhir.search`、`create`、`transaction`、`everything`、および `summary` は、同じIDを持つ[FHIR R4 API](https://developers.anpheros.com/ja/guides/store-medical-records-fhir)を提供します。
- **AI 機能。** `anpheros.context.build` は、モデル用のソースラベル付きコンテキストを準備します。[FHIR バックエンドで AI ヘルスケア アプリを構築する方法](https://developers.anpheros.com/ja/guides/build-ai-healthcare-app)。
- **本番環境。** 本番環境キー (`sk_live_…`) は、署名付き DPA を持つ検証済み組織に発行されます。[料金](https://developers.anpheros.com/ja/guides/pricing) および [制限とレート制限](https://developers.anpheros.com/ja/guides/limits) を参照してください。

## よくある質問

### 医療データ用に独自のデータベースが必要ですか？
医療データそのものについては不要です。データベースには、ユーザー、セッション、製品設定、および各ユーザーの患者IDを保存します。観察結果、状態、投薬、ドキュメント、それらの履歴、および作成者はFHIRレコードに保存されます。

### モバイルアプリはAnpherosに直接呼び出せますか？
APIキーを使用する場合、いいえ：キーはプロジェクトのすべての患者に到達するため、サーバー上に保持する必要があります。モバイルアプリは、[How to build a healthcare app with Flutter](https://developers.anpheros.com/ja/guides/healthcare-app-flutter) のように、ユーザーが自身のレコードに対して付与したOAuthトークンを使用してAnpherosに直接呼び出すことができます。

### どのフレームワークで動作しますか？
すべてです。SDKは`fetch`のみを必要とするため、Express、Fastify、Hono、NestJS、Next.jsルートハンドラー、Deno、またはBunで実行できます。

## 関連情報

- [ヘルスケアAPI](https://developers.anpheros.com/ja/guides/healthcare-api): ヘルスケアAPIに求められるもの、およびAnpheros APIの概要
- [医療データAPI](https://developers.anpheros.com/ja/guides/medical-data-api): 各種医療データがどこに格納されるか
- [FHIR R4 を使用した医療記録の保存方法](https://developers.anpheros.com/ja/guides/store-medical-records-fhir)
- [Webhook](https://developers.anpheros.com/ja/guides/webhooks) および [エラー](https://developers.anpheros.com/ja/guides/errors)
- [医療アプリのバックエンドとデータベース](https://developers.anpheros.com/ja/guides/medical-app-backend)
