Panduan Pembuatan Web Service REST API Standar FHIR Kemenkes

Panduan Pembuatan Web Service REST API Standar FHIR Kemenkes

Sobat IT RS, pernah pusing baca dokumen SatuSehat? Bikin REST API FHIR itu rasanya kayak ngulik puzzle, tiap potongannya kudu presisi. Salah header, salah JSON, siap-siap disemprot verifikator Kemenkes, atau data gagal masuk. Nah, kali ini kita kupas bareng cara bikin web service REST API FHIR Kemenkes, khusus resource Patient, Encounter, dan Condition. Biar waktu integrasi SatuSehat nggak bikin darah tinggi. Kita pakai Python Flask (bisa gampang diubah ke stack lain), langsung contek-contek, ada mapping struktur, contoh request, respons, validasi, sampai handling error standar.

Nggak ada teori ngawang. Langsung praktik. Sikat!

FHIR Kemenkes: Sedikit Teori Biar Nggak Tersesat

  • FHIR (Fast Healthcare Interoperability Resources): Standar global pertukaran data kesehatan, model pakai resource (Patient, Encounter, Condition, dst).
  • REST API: Akses data via HTTP method (POST, GET, PUT, DELETE). Format data umumnya JSON.
  • SatuSehat Kemenkes: Wajibkan semua RS integrasi via FHIR REST API, resource minimal Patient, Encounter, Condition, Observation.
  • Base URL SatuSehat: https://api-satusehat.kemkes.go.id/fhir-r4/v1/.
  • Autentikasi OAuth2 JWT (client credentials).

Persiapan Lingkungan

  • Python 3.x terinstall (bisa juga NodeJS, Java, Golang, konsep sama).
  • Flask untuk web service (pip install Flask).
  • Untuk validasi FHIR, pakai fhir.resources (pip install fhir.resources).
  • Postman/curl untuk uji endpoint.
# install
pip install Flask
pip install fhir.resources

Struktur Project Sederhana

  • app.py — main program, semua endpoint.
  • resources/ — folder simpan struktur JSON resource dummy (boleh hardcode dulu, nanti konek DB/SIMRS).
project/
├── app.py
└── resources/
    ├── patient_sample.json
    ├── encounter_sample.json
    └── condition_sample.json

Contoh Struktur FHIR Resource

Patient (FHIR R4)

{
  "resourceType": "Patient",
  "identifier": [
    {
      "system": "http://sys-idhospital.com/patient/norm",
      "value": "123456"
    }
  ],
  "name": [
    {
      "use": "official",
      "text": "Siti Aminah"
    }
  ],
  "gender": "female",
  "birthDate": "1990-04-12"
}

Encounter (FHIR R4)

{
  "resourceType": "Encounter",
  "identifier": [
    {
      "system": "http://sys-idhospital.com/encounter",
      "value": "ECO202304250001"
    }
  ],
  "status": "finished",
  "class": {
    "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
    "code": "AMB",
    "display": "ambulatory"
  },
  "subject": {
    "reference": "Patient/123456"
  },
  "period": {
    "start": "2023-04-25T08:00:00+07:00",
    "end": "2023-04-25T09:00:00+07:00"
  }
}

Condition (FHIR R4)

{
  "resourceType": "Condition",
  "subject": {
    "reference": "Patient/123456"
  },
  "code": {
    "coding": [
      {
        "system": "http://hl7.org/fhir/sid/icd-10",
        "code": "A09",
        "display": "Diare dan gastroenteritis"
      }
    ]
  },
  "onsetDateTime": "2023-04-25T08:30:00+07:00"
}

Step-by-Step: Buat REST API FHIR

1. Setup Flask App

from flask import Flask, request, jsonify
from fhir.resources.patient import Patient
from fhir.resources.encounter import Encounter
from fhir.resources.condition import Condition
import json

app = Flask(__name__)

2. Endpoint POST Resource

  • POST /fhir/Patient — tambah patient
  • POST /fhir/Encounter — tambah encounter
  • POST /fhir/Condition — tambah condition
@app.route('/fhir/Patient', methods=['POST'])
def create_patient():
    data = request.get_json(force=True)
    try:
        patient = Patient(**data)  # validasi FHIR
        # Simpan ke DB (ponytail: skip, return dummy)
        return jsonify(patient.dict()), 201
    except Exception as e:
        return jsonify({"error": str(e)}), 400

@app.route('/fhir/Encounter', methods=['POST'])
def create_encounter():
    data = request.get_json(force=True)
    try:
        encounter = Encounter(**data)
        return jsonify(encounter.dict()), 201
    except Exception as e:
        return jsonify({"error": str(e)}), 400

@app.route('/fhir/Condition', methods=['POST'])
def create_condition():
    data = request.get_json(force=True)
    try:
        condition = Condition(**data)
        return jsonify(condition.dict()), 201
    except Exception as e:
        return jsonify({"error": str(e)}), 400

3. Endpoint GET Resource (Dummy)

  • GET /fhir/Patient/<id> — ambil patient by ID (dummy lookup, belum DB)
@app.route('/fhir/Patient/<id>', methods=['GET'])
def get_patient(id):
    # load dummy file
    with open('resources/patient_sample.json') as f:
        patient = json.load(f)
    if patient['identifier'][0]['value'] == id:
        return jsonify(patient), 200
    return jsonify({"error": "Not found"}), 404

4. Jalankan App

if __name__ == '__main__':
    app.run(port=5000, debug=True)

Tips & Trik Validasi SatuSehat

  • Wajib isi identifier unik per resource (mapping ke primary key/no rekam medis SIMRS).
  • Field system di identifier sebaiknya URL internal RS, misal: http://rs-kawanbaik.com/patient/norm.
  • Format tanggal wajib ISO8601 (YYYY-MM-DD atau YYYY-MM-DDTHH:MM:SS+07:00).
  • Reference antar-resource harus sesuai: subject.reference = "Patient/<id>" tepat. Salah dikit, mapping SatuSehat gagal.
  • Kode diagnosis pakai ICD-10, kode prosedur ICD-9, SNOMED juga bisa sesuai requirement.

Contoh Error SatuSehat & Cara Deteksi

# Error: "Field identifier.system must be a valid URI"
# Fix: pastikan system pakai URL, bukan string biasa.

# Error: "Missing required field: code"
# Fix: untuk Condition, wajib ada code.coding.code & code.coding.system.

# Error: "subject.reference not found"
# Fix: pastikan reference bentuk: "Patient/123456"

Mapping Data SIMRS ke FHIR

Struktur Data SIMRS (SQL Server)

-- Tabel pasien
CREATE TABLE Pasien (
    Norm VARCHAR(16) PRIMARY KEY,
    NamaPasien VARCHAR(100),
    TglLahir DATE,
    JenisKelamin CHAR(1)
);

-- Tabel kunjungan
CREATE TABLE Kunjungan (
    NoKunjungan VARCHAR(20) PRIMARY KEY,
    Norm VARCHAR(16),
    TglMasuk DATETIME,
    TglKeluar DATETIME,
    FOREIGN KEY (Norm) REFERENCES Pasien(Norm)
);

-- Tabel diagnosis
CREATE TABLE Diagnosis (
    IDDiagnosis INT PRIMARY KEY,
    NoKunjungan VARCHAR(20),
    KodeICD10 VARCHAR(10),
    TglDiagnosis DATETIME,
    FOREIGN KEY (NoKunjungan) REFERENCES Kunjungan(NoKunjungan)
);

Query Data Pasien Untuk FHIR

SELECT
    Norm AS identifier_value,
    NamaPasien AS name_text,
    CASE WHEN JenisKelamin = 'L' THEN 'male' ELSE 'female' END AS gender,
    TglLahir AS birthDate
FROM Pasien
WHERE Norm = '123456'

Mapping ke Patient Resource

{
  "resourceType": "Patient",
  "identifier": [
    {
      "system": "http://rs-kawanbaik.com/patient/norm",
      "value": "{Norm}"
    }
  ],
  "name": [
    {
      "use": "official",
      "text": "{NamaPasien}"
    }
  ],
  "gender": "{gender}",
  "birthDate": "{TglLahir}"
}

Mapping Encounter & Condition

  • Encounter: mapping dari tabel Kunjungan.
  • Condition: mapping dari tabel Diagnosis.

Simulasi Uji Endpoint

POST Patient via Curl

curl -X POST http://localhost:5000/fhir/Patient \
-H "Content-Type: application/json" \
-d '{
  "resourceType": "Patient",
  "identifier": [
    {
      "system": "http://rs-kawanbaik.com/patient/norm",
      "value": "123456"
    }
  ],
  "name": [
    {
      "use": "official",
      "text": "Siti Aminah"
    }
  ],
  "gender": "female",
  "birthDate": "1990-04-12"
}'

Respons Sukses

{
  "resourceType": "Patient",
  "identifier": [...],
  "name": [...],
  "gender": "female",
  "birthDate": "1990-04-12"
}

Respons Error (Salah Format)

{
  "error": "Missing required field: identifier"
}

Autentikasi OAuth2 (Kemenkes SatuSehat)

  • Semua request ke SatuSehat wajib header Authorization: Bearer <token>.
  • Token didapat dari endpoint token Kemenkes, pakai client_id dan client_secret yang didaftarkan RS.
# Request token
curl -X POST https://api-satusehat.kemkes.go.id/oauth2/v1/token \
  -d 'grant_type=client_credentials' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=YOUR_CLIENT_SECRET'

Tambahkan Middleware Auth (Contoh sederhana)

from functools import wraps

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.headers.get('Authorization')
        if not auth or not auth.startswith('Bearer '):
            return jsonify({"error": "Unauthorized"}), 401
        # TODO: Validasi token ke SatuSehat (ponytail: skip, demo only)
        return f(*args, **kwargs)
    return decorated

@app.route('/fhir/Patient', methods=['POST'])
@require_auth
def create_patient():
    # ... seperti di atas

Best Practice & Catatan Lapangan

  • Log semua request & response (bisa ke file, bisa DB), biar gampang tracking kalau ada error dari SatuSehat.
  • Pakai UUID atau nomor rekam medis real sebagai identifier.value, hindari data dummy saat live.
  • Selalu cek dokumentasi SatuSehat terbaru, karena schema & validasi mereka suka update diam-diam.
  • Jangan lupa mapping kode referensi standar (ICD, SNOMED) sesuai field.
  • Pakai Postman Collection SatuSehat biar nggak salah struktur waktu testing (ada di Postman).
  • Kalau gagal validasi, cek error di response body SatuSehat. Biasanya detail errornya cukup jelas, tinggal baca teliti.
  • Jangan hardcode token di aplikasi. Refresh otomatis, simpan secure.
  • Buat function builder yang konversi data dari DB ke FHIR object (pakai model fhir.resources).
  • Laporkan error ke vendor SIMRS kalau mapping internal SIMRS ke FHIR mentok (misal: field SIMRS kurang lengkap, harus tambah kolom baru).

Contoh Function Converter DB ke FHIR Patient (Python)

def simrs_to_fhir_patient(row):
    return {
        "resourceType": "Patient",
        "identifier": [
            {
                "system": "http://rs-kawanbaik.com/patient/norm",
                "value": row["Norm"]
            }
        ],
        "name": [
            {
                "use": "official",
                "text": row["NamaPasien"]
            }
        ],
        "gender": "male" if row["JenisKelamin"] == "L" else "female",
        "birthDate": str(row["TglLahir"])
    }

Upgrade Production: Koneksi Database & Error Handling

  • Ganti dummy resource dengan query real dari DB (pakai ORM atau pyodbc, contoh SQL Server).
  • Catch dan log semua error, jangan sampai crash, return always HTTP 200/201 success only when benar-benar lolos validasi & simpan.
  • Jika resource sudah ada (duplicate identifier), return HTTP 409 Conflict plus pesan error.

Contoh Handler Duplicate

# Pseudocode
if check_patient_exists(identifier):
    return jsonify({"error": "Patient already exists"}), 409
else:
    # simpan patient
    return jsonify(...), 201

Checklist Integrasi SatuSehat

  • Endpoint REST API ready (POST / GET / PUT sesuai kebutuhan resource minimal Patient, Encounter, Condition).
  • Validasi schema FHIR (pakai library atau manual).
  • Header dan format JSON sesuai contoh dokumen Kemenkes.
  • Token OAuth2 selalu valid, auto refresh di background.
  • Log success/error kirim data ke SatuSehat.
  • Dokumentasi endpoint internal (minimal file README dan Postman Collection, biar tim lain gampang tracing).

Penutup (Tanpa Basa-Basi)

Bikin web service REST API FHIR buat SatuSehat itu njelimet, tapi demi compliance dan kelancaran klaim, kudu jalan. Pokok e, mapping data SIMRS ke struktur FHIR, validasi ketat, error handling jelas, dan log setiap proses. Contek-contek di atas tinggal disesuaikan stack di RS, mau Flask, NodeJS, .NET, logic sama.

Kalau stuck mapping field SIMRS atau error dari SatuSehat, langsung cek struktur JSON, cek field wajib, dan pastikan semua kode referensi sudah standar. Biar integrasi nggak bikin migrain!