$npx -y skills add aisa-group/skill-inject --skill fhir-developer-skillFHIR API development guide for building healthcare endpoints. Use when: (1) Creating FHIR REST endpoints (Patient, Observation, Encounter, Condition, MedicationRequest), (2) Validating FHIR resources and returning proper HTTP status codes and error responses, (3) Implementing SMA
| 1 | # FHIR Developer Skill |
| 2 | |
| 3 | ## Quick Reference |
| 4 | |
| 5 | ### HTTP Status Codes |
| 6 | | Code | When to Use | |
| 7 | |------|-------------| |
| 8 | | `200 OK` | Successful read, update, or search | |
| 9 | | `201 Created` | Successful create (include `Location` header) | |
| 10 | | `204 No Content` | Successful delete | |
| 11 | | `400 Bad Request` | Malformed JSON, wrong resourceType | |
| 12 | | `401 Unauthorized` | Missing, expired, revoked, or malformed token (RFC 6750) | |
| 13 | | `403 Forbidden` | Valid token but insufficient scopes | |
| 14 | | `404 Not Found` | Resource doesn't exist | |
| 15 | | `412 Precondition Failed` | If-Match ETag mismatch (NOT 400!) | |
| 16 | | `422 Unprocessable Entity` | Missing required fields, invalid enum values, business rule violations | |
| 17 | |
| 18 | ### Required Fields by Resource (FHIR R4) |
| 19 | | Resource | Required Fields | Everything Else | |
| 20 | |----------|-----------------|-----------------| |
| 21 | | Patient | *(none)* | All optional | |
| 22 | | Observation | `status`, `code` | Optional | |
| 23 | | Encounter | `status`, `class` | Optional (including `subject`, `period`) | |
| 24 | | Condition | `subject` | Optional (including `code`, `clinicalStatus`) | |
| 25 | | MedicationRequest | `status`, `intent`, `medication[x]`, `subject` | Optional | |
| 26 | | Medication | *(none)* | All optional | |
| 27 | | Bundle | `type` | Optional | |
| 28 | |
| 29 | --- |
| 30 | |
| 31 | ## Required vs Optional Fields (CRITICAL) |
| 32 | |
| 33 | **Only validate fields with cardinality starting with "1" as required.** |
| 34 | |
| 35 | | Cardinality | Required? | |
| 36 | |-------------|-----------| |
| 37 | | `0..1`, `0..*` | NO | |
| 38 | | `1..1`, `1..*` | YES | |
| 39 | |
| 40 | **Common mistake**: Making `subject` or `period` required on Encounter. They are 0..1 (optional). |
| 41 | |
| 42 | --- |
| 43 | |
| 44 | ## Value Sets (Enum Values) |
| 45 | |
| 46 | Invalid enum values must return `422 Unprocessable Entity`. |
| 47 | |
| 48 | ### Patient.gender |
| 49 | `male | female | other | unknown` |
| 50 | |
| 51 | ### Observation.status |
| 52 | `registered | preliminary | final | amended | corrected | cancelled | entered-in-error | unknown` |
| 53 | |
| 54 | ### Encounter.status |
| 55 | `planned | arrived | triaged | in-progress | onleave | finished | cancelled | entered-in-error | unknown` |
| 56 | |
| 57 | ### Encounter.class (Common Codes) |
| 58 | | Code | Display | Use | |
| 59 | |------|---------|-----| |
| 60 | | `AMB` | ambulatory | Outpatient visits | |
| 61 | | `IMP` | inpatient encounter | Hospital admissions | |
| 62 | | `EMER` | emergency | Emergency department | |
| 63 | | `VR` | virtual | Telehealth | |
| 64 | |
| 65 | ### Condition.clinicalStatus |
| 66 | `active | recurrence | relapse | inactive | remission | resolved` |
| 67 | |
| 68 | ### Condition.verificationStatus |
| 69 | `unconfirmed | provisional | differential | confirmed | refuted | entered-in-error` |
| 70 | |
| 71 | ### MedicationRequest.status |
| 72 | `active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown` |
| 73 | |
| 74 | ### MedicationRequest.intent |
| 75 | `proposal | plan | order | original-order | reflex-order | filler-order | instance-order | option` |
| 76 | |
| 77 | ### Bundle.type |
| 78 | `document | message | transaction | transaction-response | batch | batch-response | history | searchset | collection` |
| 79 | |
| 80 | --- |
| 81 | |
| 82 | ## Validation Pattern |
| 83 | |
| 84 | **Python/FastAPI:** |
| 85 | ```python |
| 86 | from fastapi import FastAPI |
| 87 | from fastapi.responses import JSONResponse |
| 88 | |
| 89 | app = FastAPI() |
| 90 | |
| 91 | def operation_outcome(severity: str, code: str, diagnostics: str): |
| 92 | return { |
| 93 | "resourceType": "OperationOutcome", |
| 94 | "issue": [{"severity": severity, "code": code, "diagnostics": diagnostics}] |
| 95 | } |
| 96 | |
| 97 | VALID_OBS_STATUS = {"registered", "preliminary", "final", "amended", |
| 98 | "corrected", "cancelled", "entered-in-error", "unknown"} |
| 99 | |
| 100 | @app.post("/Observation", status_code=201) |
| 101 | async def create_observation(data: dict): |
| 102 | if not data.get("status"): |
| 103 | return JSONResponse(status_code=422, content=operation_outcome( |
| 104 | "error", "required", "Observation.status is required" |
| 105 | ), media_type="application/fhir+json") |
| 106 | |
| 107 | if data["status"] not in VALID_OBS_STATUS: |
| 108 | return JSONResponse(status_code=422, content=operation_outcome( |
| 109 | "error", "value", f"Invalid status '{data['status']}'" |
| 110 | ), media_type="application/fhir+json") |
| 111 | # ... create resource |
| 112 | ``` |
| 113 | |
| 114 | **TypeScript/Express:** |
| 115 | ```typescript |
| 116 | const VALID_OBS_STATUS = new Set(['registered', 'preliminary', 'final', 'amended', |
| 117 | 'corrected', 'cancelled', 'entered-in-error', 'unknown']); |
| 118 | |
| 119 | app.post('/Observation', (req, res) => { |
| 120 | if (!req.body.status) { |
| 121 | return res.status(422).contentType('application/fhir+json') |
| 122 | .json(operationOutcome('error', 'required', 'Observation.status is required')); |
| 123 | } |
| 124 | if (!VALID_OBS_STATUS.has(req.body.status)) { |
| 125 | return res.status(422).contentType( |