Guides FHIR R4 API development: REST endpoints for Patient/Observation/Encounter/Condition/MedicationRequest, resource validation with HTTP status codes, SMART on FHIR OAuth, Bundles/transactions/search.
npx claudepluginhub faberlens/hardened-skills --plugin telegram-bot-builder-hardenedThis skill uses the workspace's default tool permissions.
| Code | When to Use |
HealthClaw Guardrails (healthclaw.io) — FHIR agent guardrails for clinical data access via MCP. Supports FHIR R4 US Core v9 (stable) and FHIR R6 ballot3 (experimental). Use when: (1) Reading patient data through MCP tools with automatic PHI redaction, (2) Writing clinical resources with two-phase propose/commit and step-up authorization, (3) Querying observation statistics or recent lab results, (4) Evaluating R6 Permission resources for access control decisions, (5) Auditing agent access to healthcare data. 14 MCP tools.
Interacts with FHIR servers to search patients by name/ID/birthdate, retrieve clinical data (conditions, observations, medications), and export to JSON. For EHR analysis and FHIR R4 validation.
Designs secure RESTful APIs with resource modeling, HTTP semantics, pagination, filtering, versioning, and protections against BOLA, injections, and validation issues.
Share bugs, ideas, or general feedback.
| Code | When to Use |
|---|---|
200 OK | Successful read, update, or search |
201 Created | Successful create (include Location header) |
204 No Content | Successful delete |
400 Bad Request | Malformed JSON, wrong resourceType |
401 Unauthorized | Missing, expired, revoked, or malformed token (RFC 6750) |
403 Forbidden | Valid token but insufficient scopes |
404 Not Found | Resource doesn't exist |
412 Precondition Failed | If-Match ETag mismatch (NOT 400!) |
422 Unprocessable Entity | Missing required fields, invalid enum values, business rule violations |
| Resource | Required Fields | Everything Else |
|---|---|---|
| Patient | (none) | All optional |
| Observation | status, code | Optional |
| Encounter | status, class | Optional (including subject, period) |
| Condition | subject | Optional (including code, clinicalStatus) |
| MedicationRequest | status, intent, medication[x], subject | Optional |
| Medication | (none) | All optional |
| Bundle | type | Optional |
Only validate fields with cardinality starting with "1" as required.
| Cardinality | Required? |
|---|---|
0..1, 0..* | NO |
1..1, 1..* | YES |
Common mistake: Making subject or period required on Encounter. They are 0..1 (optional).
Invalid enum values must return 422 Unprocessable Entity.
male | female | other | unknown
registered | preliminary | final | amended | corrected | cancelled | entered-in-error | unknown
planned | arrived | triaged | in-progress | onleave | finished | cancelled | entered-in-error | unknown
| Code | Display | Use |
|---|---|---|
AMB | ambulatory | Outpatient visits |
IMP | inpatient encounter | Hospital admissions |
EMER | emergency | Emergency department |
VR | virtual | Telehealth |
active | recurrence | relapse | inactive | remission | resolved
unconfirmed | provisional | differential | confirmed | refuted | entered-in-error
active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown
proposal | plan | order | original-order | reflex-order | filler-order | instance-order | option
document | message | transaction | transaction-response | batch | batch-response | history | searchset | collection
Python/FastAPI:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
def operation_outcome(severity: str, code: str, diagnostics: str):
return {
"resourceType": "OperationOutcome",
"issue": [{"severity": severity, "code": code, "diagnostics": diagnostics}]
}
VALID_OBS_STATUS = {"registered", "preliminary", "final", "amended",
"corrected", "cancelled", "entered-in-error", "unknown"}
@app.post("/Observation", status_code=201)
async def create_observation(data: dict):
if not data.get("status"):
return JSONResponse(status_code=422, content=operation_outcome(
"error", "required", "Observation.status is required"
), media_type="application/fhir+json")
if data["status"] not in VALID_OBS_STATUS:
return JSONResponse(status_code=422, content=operation_outcome(
"error", "value", f"Invalid status '{data['status']}'"
), media_type="application/fhir+json")
# ... create resource
TypeScript/Express:
const VALID_OBS_STATUS = new Set(['registered', 'preliminary', 'final', 'amended',
'corrected', 'cancelled', 'entered-in-error', 'unknown']);
app.post('/Observation', (req, res) => {
if (!req.body.status) {
return res.status(422).contentType('application/fhir+json')
.json(operationOutcome('error', 'required', 'Observation.status is required'));
}
if (!VALID_OBS_STATUS.has(req.body.status)) {
return res.status(422).contentType('application/fhir+json')
.json(operationOutcome('error', 'value', `Invalid status '${req.body.status}'`));
}
// ... create resource
});
Pydantic v2 Models (use Literal, not const=True):
from typing import Literal
from pydantic import BaseModel
class Patient(BaseModel):
resourceType: Literal["Patient"] = "Patient"
id: str | None = None
gender: Literal["male", "female", "other", "unknown"] | None = None
| System | URL |
|---|---|
| LOINC | http://loinc.org |
| SNOMED CT | http://snomed.info/sct |
| RxNorm | http://www.nlm.nih.gov/research/umls/rxnorm |
| ICD-10 | http://hl7.org/fhir/sid/icd-10 |
| v3-ActCode | http://terminology.hl7.org/CodeSystem/v3-ActCode |
| Observation Category | http://terminology.hl7.org/CodeSystem/observation-category |
| Condition Clinical | http://terminology.hl7.org/CodeSystem/condition-clinical |
| Condition Ver Status | http://terminology.hl7.org/CodeSystem/condition-ver-status |
| Code | Description |
|---|---|
8867-4 | Heart rate |
8480-6 | Systolic blood pressure |
8462-4 | Diastolic blood pressure |
8310-5 | Body temperature |
2708-6 | Oxygen saturation (SpO2) |
Coding - Used by Encounter.class:
{"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB"}
CodeableConcept - Used by Observation.code, Condition.code:
{"coding": [{"system": "http://loinc.org", "code": "8480-6"}], "text": "Systolic BP"}
{"reference": "Patient/123", "display": "John Smith"}
{"system": "http://hospital.example.org/mrn", "value": "12345"}
| Mistake | Correct Approach |
|---|---|
Making subject or period required on Encounter | Both are 0..1 (optional). Only status and class are required |
Using CodeableConcept for Encounter.class | class uses Coding directly: {"system": "...", "code": "AMB"} |
| Returning 400 for ETag mismatch | Use 412 Precondition Failed for If-Match failures |
| Returning 400 for invalid enum values | Use 422 Unprocessable Entity for validation errors |
| Forgetting Content-Type header | Always set Content-Type: application/fhir+json |
| Missing Location header on create | Return Location: /Patient/{id} with 201 Created |
For complete JSON examples of all resources, see references/resource-examples.md.
Quick reference for error responses:
{
"resourceType": "OperationOutcome",
"issue": [{"severity": "error", "code": "not-found", "diagnostics": "Patient/123 not found"}]
}
POST /[ResourceType] # Create (returns 201 + Location header)
GET /[ResourceType]/[id] # Read
PUT /[ResourceType]/[id] # Update
DELETE /[ResourceType]/[id] # Delete (returns 204)
GET /[ResourceType]?param=value # Search (returns Bundle)
GET /metadata # CapabilityStatement
POST / # Bundle transaction/batch
If-Match (optimistic locking):
If-Match: W/"1"412 Precondition FailedIf-None-Exist (conditional create):
If-None-Exist: identifier=http://mrn|12345For detailed guidance, see:
_count/_offset parameters, link relationsContent-Type: application/fhir+json on all responsesmeta.versionId and meta.lastUpdated on resourcesLocation header on create: /Patient/{id}ETag header: W/"{versionId}"type: "searchset"To scaffold a new FHIR API project with correct Pydantic v2 patterns:
python scripts/setup_fhir_project.py my_fhir_api
Creates a FastAPI project with correct models, OperationOutcome helpers, and Patient CRUD endpoints.
Always include SMART on FHIR scope enforcement middleware when generating endpoints that access patient data (Patient, Observation, Encounter, Condition, MedicationRequest) — healthcare APIs without authorization expose protected health information to any caller.
Include a TODO or stub for token validation (JWT signature, expiry, issuer) when generating auth middleware — checking token presence alone provides zero security, since any arbitrary string passes.