# TypeScript로 의료 API 구축 방법

> 튜토리얼: Express API를 TypeScript로 구현하여 환자 기록 생성, 혈압 측정값을 FHIR R4 관찰 항목으로 저장, 다시 읽어오기, 오류 매핑 및 웹훅 검증 — 자체 의료 데이터베이스 없이 진행합니다.

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

**이 튜토리얼에서는 Express를 사용하여 TypeScript로 혈압 추적 앱의 백엔드 API를 구축합니다. 회원가입 시 사용자의 의료 기록이 생성되고, 측정값은 코딩된 HL7 FHIR R4 관찰 항목으로 저장되며, 앱은 이를 다시 읽어오고, 웹훅 엔드포인트는 새로운 데이터를 수신합니다. 자체 의료 데이터베이스는 필요하지 않습니다.** API는 제품별 고유 정보(사용자, 세션, 설정)를 유지합니다. 의료 데이터, 기록, 동의 및 출처는 Anpheros에 저장되며, [TypeScript SDK](https://developers.anpheros.com/ko/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/ko/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`의 혈압 측정값 하나와 두 개의 구성 요소를 저장합니다.

```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"}}` 오류를 반환합니다. 속도 제한(`429`) 및 서버 오류는 SDK에서 재시도한 후 표시됩니다. `requestId`을 기록하고 지원팀에 문의할 경우 해당 내용을 인용하십시오. 전체 목록은 [오류](https://developers.anpheros.com/ko/guides/errors)에서 확인할 수 있습니다.

## 6단계: 웹훅으로 새 데이터 알림 받기

다른 곳에서 새로운 Readings가 도착할 수도 있습니다. 예를 들어, 같은 레코드에 쓰는 클리닉, 검사 보고서, 사용자가 연결한 다른 앱 등입니다. 엔드포인트를 한 번 등록하세요:

```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`입니다. 전달은 서명되고, 재시도되며, 다시 재생될 수 있습니다([웹훅](https://developers.anpheros.com/ko/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/ko/guides/smart-on-fhir-backend).
- **필요한 경우 엄격한 FHIR 사용.** `anpheros.fhir.search`, `create`, `transaction`, `everything` 및 `summary`는 동일한 ID를 가진 [FHIR R4 API](https://developers.anpheros.com/ko/guides/store-medical-records-fhir)를 제공합니다.
- **AI 기능.** `anpheros.context.build`는 모델을 위한 소스 레이블이 지정된 컨텍스트를 준비합니다: [FHIR 백엔드로 AI 의료 앱 구축 방법](https://developers.anpheros.com/ko/guides/build-ai-healthcare-app).
- **운영 환경.** 운영 환경 키(`sk_live_…`)는 서명된 DPA가 있는 검증된 조직에 발급됩니다. [가격](https://developers.anpheros.com/ko/guides/pricing) 및 [제한 사항 및 속도 제한](https://developers.anpheros.com/ko/guides/limits)을 참조하십시오.

## 자주 묻는 질문

### 의료 데이터를 위한 자체 데이터베이스가 필요합니까?
의료 데이터 자체를 위해서는 필요하지 않습니다. 사용자의 데이터베이스에는 사용자, 세션, 제품 설정 및 각 사용자의 환자 ID가 저장됩니다. 관찰, 상태, 약물, 문서, 해당 기록 및 작성자는 FHIR 레코드에 저장됩니다.

### 모바일 앱이 Anpheros를 직접 호출할 수 있습니까?
API 키로는 불가능합니다. 키는 프로젝트의 모든 환자에게 도달하며 서버에 유지되어야 합니다. 모바일 앱은 [How to build a healthcare app with Flutter](https://developers.anpheros.com/ko/guides/healthcare-app-flutter)에서와 같이 개인이 자신의 기록에 대해 부여한 OAuth 토큰으로 Anpheros를 직접 호출할 수 있습니다.

### 어떤 프레임워크와 함께 작동합니까?
모든 프레임워크와 함께 작동합니다. SDK는 `fetch`만 필요하므로 Express, Fastify, Hono, NestJS, Next.js 라우트 핸들러, Deno 또는 Bun에서 실행됩니다.

## 관련 자료

- [Healthcare API](https://developers.anpheros.com/ko/guides/healthcare-api): 헬스케어 API에서 확인해야 할 사항 및 Anpheros API 개요
- [Medical data API](https://developers.anpheros.com/ko/guides/medical-data-api): 각 종류의 의료 데이터가 어디로 가는지
- [FHIR R4를 사용하여 의료 기록 저장 방법](https://developers.anpheros.com/ko/guides/store-medical-records-fhir)
- [Webhooks](https://developers.anpheros.com/ko/guides/webhooks) 및 [Errors](https://developers.anpheros.com/ko/guides/errors)
- [Medical app backend and database](https://developers.anpheros.com/ko/guides/medical-app-backend)
