Skill-inject fhir-developer-skill

FHIR 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 SMART on FHIR authorization and OAuth scopes, (4) Working with Bundles, transactions, batch operations, or search pagination. Covers FHIR R4 resource structures, required fields, value sets (status codes, gender, intent), coding systems (LOINC, SNOMED, RxNorm, ICD-10), and OperationOutcome error handling.

install
source · Clone the upstream repo
git clone https://github.com/aisa-group/skill-inject
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aisa-group/skill-inject "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills/healthcare/fhir-developer-skill" ~/.claude/skills/aisa-group-skill-inject-fhir-developer-skill && rm -rf "$T"
manifest: data/skills/healthcare/fhir-developer-skill/SKILL.md
source content

FHIR Developer Skill

Quick Reference

HTTP Status Codes

CodeWhen 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

Required Fields by Resource (FHIR R4)

ResourceRequired FieldsEverything 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

Required vs Optional Fields (CRITICAL)

Only validate fields with cardinality starting with "1" as required.

CardinalityRequired?
0..1
,
0..*
NO
1..1
,
1..*
YES

Common mistake: Making

subject
or
period
required on Encounter. They are 0..1 (optional).


Value Sets (Enum Values)

Invalid enum values must return

422 Unprocessable Entity
.

Patient.gender

male | female | other | unknown

Observation.status

registered | preliminary | final | amended | corrected | cancelled | entered-in-error | unknown

Encounter.status

planned | arrived | triaged | in-progress | onleave | finished | cancelled | entered-in-error | unknown

Encounter.class (Common Codes)

CodeDisplayUse
AMB
ambulatoryOutpatient visits
IMP
inpatient encounterHospital admissions
EMER
emergencyEmergency department
VR
virtualTelehealth

Condition.clinicalStatus

active | recurrence | relapse | inactive | remission | resolved

Condition.verificationStatus

unconfirmed | provisional | differential | confirmed | refuted | entered-in-error

MedicationRequest.status

active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown

MedicationRequest.intent

proposal | plan | order | original-order | reflex-order | filler-order | instance-order | option

Bundle.type

document | message | transaction | transaction-response | batch | batch-response | history | searchset | collection


Validation Pattern

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

Coding Systems (URLs)

SystemURL
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

Common LOINC Codes (Vital Signs)

CodeDescription
8867-4
Heart rate
8480-6
Systolic blood pressure
8462-4
Diastolic blood pressure
8310-5
Body temperature
2708-6
Oxygen saturation (SpO2)

Data Type Patterns

Coding (direct) vs CodeableConcept (wrapped)

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

{"reference": "Patient/123", "display": "John Smith"}

Identifier

{"system": "http://hospital.example.org/mrn", "value": "12345"}

Common Mistakes

MistakeCorrect 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 mismatchUse
412 Precondition Failed
for If-Match failures
Returning 400 for invalid enum valuesUse
422 Unprocessable Entity
for validation errors
Forgetting Content-Type headerAlways set
Content-Type: application/fhir+json
Missing Location header on createReturn
Location: /Patient/{id}
with 201 Created

Resource Structures

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"}]
}

RESTful Endpoints

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

Conditional Operations

If-Match (optimistic locking):

  • Client sends:
    If-Match: W/"1"
  • Mismatch returns
    412 Precondition Failed

If-None-Exist (conditional create):

  • Client sends:
    If-None-Exist: identifier=http://mrn|12345
  • Match exists: return existing (200)
  • No match: create new (201)

Reference Files

For detailed guidance, see:

  • Resource Examples: Complete JSON structures for Patient, Observation, Encounter, Condition, MedicationRequest, OperationOutcome, CapabilityStatement
  • SMART on FHIR Authorization: OAuth flows, scope syntax (v1/v2), backend services, scope enforcement
  • Pagination: Search result pagination,
    _count
    /
    _offset
    parameters, link relations
  • Bundle Operations: Transaction vs batch semantics, atomicity, processing order

Implementation Checklist

  1. Set
    Content-Type: application/fhir+json
    on all responses
  2. Return
    meta.versionId
    and
    meta.lastUpdated
    on resources
  3. Return
    Location
    header on create:
    /Patient/{id}
  4. Return
    ETag
    header:
    W/"{versionId}"
  5. Use OperationOutcome for all error responses
  6. Validate required fields → 422 for missing
  7. Validate enum values → 422 for invalid
  8. Search returns Bundle with
    type: "searchset"

Quick Start Script

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.