Lesson 0013 · FHIR · Modul Bulk Data · ±23 menit

Tiga level $export; alur kick-off → 202 → polling → manifest

Lesson pertama modul Bulk Data: kenapa search biasa tidak cukup untuk jutaan resource, dan bagaimana ekspor asinkron berjalan

Sebelumnya, Review 1 menutup modul Fondasi dan Resource. Modul ini membahas JSON massal: dari "ekspor jutaan resource" sampai "insert ke database tanpa kehabisan memori". Lesson ini mulai dari sisi server FHIR: endpoint $export dan alur asinkronnya.

Target lesson ini: Anda bisa menjawab "Bagaimana Anda mengekspor jutaan resource dari server FHIR?" tanpa catatan, dalam 90 detik: tiga level $export, kick-off dengan Prefer: respond-async, 202 + Content-Location, polling dengan Retry-After/429, sampai 200 + manifest.

Semua fakta Bulk Data di lesson ini diambil dari Bulk Data Access IG v2.0.0 (STU 2), yang dibangun di atas FHIR R4. Halaman IG itu sendiri mencatat bahwa versi 3.0.0 sudah menggantikannya [1]; alur intinya sama, tapi sebutkan versinya kalau interviewer bertanya detail. Search dan paging biasa ada di lesson 0008 dan 0009; autentikasi SMART Backend Services dibahas di lesson 0019. Lesson ini tanpa lab: potongan HTTP, JSON, dan TypeScript hanya untuk dibaca dan dijelaskan.

1. Kenapa bukan search biasa: tiga level $export

Search dengan paging (GET Observation?_count=1000 lalu ikuti link next) cocok untuk layar aplikasi, bukan untuk jutaan resource: satu request per halaman, dan Anda yang mengatur semuanya. Bulk Data $export membalik perannya: server menyiapkan file di belakang, client mengambil hasilnya nanti. Operasi ini "initiates the asynchronous generation of a requested export dataset - whether that be data for all patients, data for a subset (defined group) of patients, or all FHIR data in the server." [1]

LevelEndpointIsiContoh kebutuhan
Patient[base]/Patient/$exportdata semua pasienmigrasi rekam medis ke sistem baru
Group[base]/Group/[id]/$exportdata anggota satu Groupkohort pasien diabetes untuk analitik, roster pasien dari payer
System[base]/$exportsemua data, terkait pasien atau tidakbackup server, ekspor terminologi dengan _type

Kutipan tabel: [1]. Untuk level Patient dan Group, Patient Compartment dipakai sebagai acuan resource yang dikembalikan; resource pendukung seperti Organization dan Practitioner MAY ikut.

Tiga hal yang perlu disebut saat menjelaskan kick-off:

Latihan 1 — Level dan kick-off

Jawab tanpa scroll ke atas. Soal yang sama akan muncul lagi, diacak, di review campuran 0021.

Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil review 0021.

Endpoint dan header kick-off sudah jelas. Berikutnya: apa yang terjadi setelah server menerima kick-off.

2. Alur asinkron: kick-off → 202 → polling → manifest

token (Backend Services)→ kick-off→ 202 + Content-Location→ polling (202…)→ 200 + manifest→ unduh NDJSON
GET [base]/Patient/$export?_type=Patient,Observation&_since=2026-10-25T00:00:00Z
Accept: application/fhir+json
Prefer: respond-async
Authorization: Bearer eyJhbGciOi…          ← token dari SMART Backend Services (0019)

HTTP/1.1 202 Accepted
Content-Location: https://fhir.example.org/bulkstatus/42   ← URL status untuk polling

GET https://fhir.example.org/bulkstatus/42
  → 202 Accepted           X-Progress: "50% complete"   Retry-After: 120
  → 429 Too Many Requests  Retry-After: 300              (kalau polling terlalu sering)
  → 200 OK                 Expires: …   Content-Type: application/json   + body manifest
  → 4XX/5XX                body OperationOutcome          (export gagal)

Versi TypeScript singkat dari sisi client (untuk dibaca, bukan dijalankan):

async function waitForManifest(statusUrl: string, token: string): Promise<Manifest> {
  let delay = 10;                                          // detik
  for (;;) {
    const res = await fetch(statusUrl, {
      headers: { Accept: "application/json", Authorization: `Bearer ${token}` },
    });
    if (res.status === 200) return (await res.json()) as Manifest;   // manifest kecil: json() aman
    if (res.status !== 202 && res.status !== 429) {
      throw new Error(`export gagal: ${res.status}`);     // body: OperationOutcome
    }
    const retryAfter = Number(res.headers.get("Retry-After"));       // http-date → NaN
    delay = retryAfter > 0 ? retryAfter : Math.min(delay * 2, 600);  // hormati server, else backoff
    await new Promise(r => setTimeout(r, delay * 1000));
  }
}

Padanan yang mungkin Anda kenal: pola "job + status URL" yang sama dengan antrean job (BullMQ) atau API laporan asinkron. IG menyebutnya "FHIR Asynchronous Request Pattern". [1]

Latihan 2 — Susun urutan alur $export

Urutkan langkahnya dari awal sampai data masuk database. Gunakan ↑/↓, lalu Periksa.

  1. Client meminta access token lewat SMART Backend Services
  2. GET [base]/Group/[id]/$export dengan Prefer: respond-async
  3. Server membalas 202 Accepted + Content-Location
  4. Client polling URL status; menunggu sesuai Retry-After selama masih 202
  5. Server membalas 200 OK dengan manifest JSON
  6. Client mengunduh setiap URL di output dan error sebelum Expires
  7. Setiap file NDJSON di-stream, di-parse per baris, di-insert per batch
  8. transactionTime disimpan sebagai _since untuk export berikutnya

Token dulu (kick-off, status, dan file dengan requiresAccessToken: true dilindungi dengan cara yang sama), lalu kick-off → 202 → polling → 200 + manifest → unduh → proses streaming. transactionTime adalah jam server saat query dijalankan, jadi aman dipakai sebagai _since berikutnya.

Latihan 3 — Ucapkan dengan suara keras

Pasang timer dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.

Jawaban contoh dan checklist di bawah juga menyebut isi manifest, _type, dan _since; detailnya ada di lesson berikutnya, 0014.

"Bagaimana Anda mengekspor jutaan resource dari server FHIR? Jelaskan alurnya dari kick-off sampai file diunduh." (90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya

"Saya tidak memakai paging search biasa, tapi Bulk Data $export. Ada tiga level: Patient/$export untuk semua pasien, Group/[id]/$export untuk satu kohort, dan $export di base untuk seluruh server. Client mendapat token lewat SMART Backend Services, lalu kick-off dengan Prefer: respond-async, opsional _type untuk membatasi tipe dan _since untuk export inkremental. Server membalas 202 Accepted dengan header Content-Location, yaitu URL status. Client polling URL itu: selama masih 202, ikuti Retry-After dan exponential backoff; kalau terlalu sering, server membalas 429. Setelah selesai, 200 dengan manifest JSON: transactionTime, requiresAccessToken, array output berisi URL file NDJSON per tipe resource, dan array error berisi OperationOutcome. File diunduh sebelum Expires, diproses secara streaming, dan transactionTime disimpan sebagai _since untuk export berikutnya."

Checklist: apakah Anda menyebut…

  • tiga level: Patient/$export, Group/[id]/$export, system $export?
  • kick-off asinkron dengan Prefer: respond-async, autentikasi lewat SMART Backend Services?
  • 202 Accepted + Content-Location sebagai URL status?
  • polling: 202 selama berjalan, hormati Retry-After/backoff, 429 kalau terlalu sering?
  • 200 + manifest: output (URL file NDJSON per tipe), error, transactionTime?
  • _type untuk membatasi tipe dan _since = transactionTime sebelumnya untuk inkremental?

Lulus jika minimal 4 dari 6.

Berikutnya, lesson 0014: isi manifest, format NDJSON, parameter _type/_since, dan kenapa file hasilnya tidak boleh di-JSON.parse sekaligus.

Istilah: GLOSSARY.md (bagian "Bulk Data dan JSON skala besar") · Indeks: semua lesson FHIR

Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "apa beda Patient/$export dan Group/[id]/$export untuk use case analitik?", "bagaimana menulis hasil export ke S3 dengan backpressure yang benar?", atau "kapan lebih baik memakai worker thread daripada streaming?"

Sumber: [1] Bulk Data Access IG v2.0.0 (STU 2): Export