Skip to main content
TLQ FHIR Server (TLQ FHIR) supports FHIR Bundles as multi-request envelopes when you POST a Bundle to the server base endpoint (POST /fhir).
  • Bundle.type = batch: entries are independent (partial success is expected)
  • Bundle.type = transaction: atomic (all-or-nothing), with intra-transaction reference rewriting
This page documents TLQ FHIR-specific behavior, especially the transaction mechanics.

Quick Comparison

Aspectbatchtransaction
Atomic❌ No✅ Yes
Inter-entry references via fullUrl❌ Rejected (non-conformant)✅ Supported (rewritten)
Conditional references (Patient?identifier=...)✅ Supported (must resolve to exactly one match)✅ Supported (must resolve to exactly one match)
Overall HTTP status200 (with per-entry statuses)200 on success; non-2xx on any failure
Processing orderDELETE → POST → PUT/PATCH → GET/HEADDELETE → POST → PUT/PATCH → (finalize refs) → GET/HEAD
Conditional ops (If-None-Exist / criteria URLs)✅ Supported✅ Supported (resolved inside the DB transaction)

Endpoint and Request Shape

Send a Bundle to the base endpoint:
curl -X POST "http://localhost:8080/fhir" \
  -H "Content-Type: application/fhir+json" \
  -d '{ "resourceType": "Bundle", "type": "transaction", "entry": [] }'
The handler accepts:
  • type: "batch"
  • type: "transaction"
  • type: "history" (replication; not covered here)
In Bundle.entry.request.url, TLQ FHIR accepts both relative and absolute URLs (it strips scheme/host before parsing). Example: Patient/123 and https://example.com/fhir/Patient/123 are treated the same.

Response Detail Level (Prefer)

TLQ FHIR supports these response styles for both batch and transaction:
  • Prefer: return=minimal → status/location/etag only
  • Prefer: return=representation → include the resource bodies in the response bundle
  • Prefer: return=OperationOutcome → include an OperationOutcome in entry.response.outcome

Batch Bundles (type=batch)

Batch entries are processed independently (FHIR “batch” semantics).

Independence rules TLQ FHIR enforces

TLQ FHIR pre-validates and flags non-conformant patterns per entry:
  • No reference resolution between entries: if a resource contains Reference.reference equal to a fullUrl of a POST entry in the same batch, that entry is rejected.
  • No change-interdependencies: multiple PUT/PATCH/DELETE entries targeting the same {type}/{id} are rejected.
If you need entries to reference each other, use a transaction bundle.

Error shape

Batch requests typically return HTTP 200 with a Bundle.type = batch-response. Errors are represented per entry via entry.response.status and an OperationOutcome.

Transaction Bundles (type=transaction)

Transactions are atomic: TLQ FHIR runs the entire bundle inside a single database transaction.
  • If every entry succeeds → commit and return Bundle.type = transaction-response (HTTP 200).
  • If any entry fails → rollback and return a normal error response (non-2xx) and no changes are persisted.
Error messages include entry context like Transaction entry {index}: ... to help you pinpoint which entry triggered the rollback.

Processing order (important)

TLQ FHIR processes entries in this order (regardless of original order):
  1. DELETE
  2. POST (create)
  3. PUT / PATCH (update)
  4. Finalize “resolve-as-version-specific” references (see below)
  5. GET / HEAD (read)
This order matches the implementation in server/src/services/transaction.rs.

Constraints TLQ FHIR enforces

  • No duplicate fullUrl values across entries.
  • No identity overlaps for change interactions: you can’t have multiple DELETE/PUT/PATCH entries targeting the same {type}/{id} inside one transaction (to avoid order-dependent outcomes).
  • GET/HEAD inside a transaction is instance-read only; if the URL has no id (looks like a type-level search), TLQ FHIR returns an empty searchset bundle entry.

fullUrl mapping and intra-transaction reference rewriting

Transactions support the common FHIR pattern:
  • Create a resource in one entry (POST)
  • Reference it from another entry using the first entry’s fullUrl
TLQ FHIR implements this by building a mapping and rewriting resources before writing them:
  1. For non-POST entries, if entry.fullUrl exists and the request URL is an identity ({type}/{id}), TLQ FHIR maps fullUrl → "{type}/{id}".
  2. For POST entries, TLQ FHIR pre-reserves UUID ids and maps fullUrl → "{type}/{uuid}" before processing entries, so later entries can safely reference the would-be created resources.
  3. Before POST/PUT/PATCH, TLQ FHIR rewrites string values inside the resource JSON:
    • Exact fullUrl matches are replaced (fragment-aware: urn:...#frag works).
    • Generic string replacement is also applied (e.g., narrative strings), except under canonical element paths.
Canonical fields are intentionally excluded from rewriting so canonical URLs don’t accidentally get rewritten when they happen to contain a fullUrl substring.

Conditional references inside resources

TLQ FHIR supports conditional references (FHIR “search URIs”) inside request resources: Example:
<subject>
  <reference value="Patient?identifier=http://example.org/fhir/mrn|12345"/>
</subject>
Rules (FHIR-style):
  • TLQ FHIR searches the target type (Patient in the example) using the query string parameters.
  • If there are 0 matches or >1 match, the interaction fails:
    • transaction: the transaction fails (and everything rolls back)
    • batch: the entry fails with 412 (other entries are unaffected)
  • If there is exactly 1 match, TLQ FHIR replaces the search URI with a normal reference (Patient/{id}).
See also: CRUD Operations → Conditional References.

Conditional operations inside a transaction

TLQ FHIR supports the common conditional patterns and resolves them within the transaction, meaning: searches see the effects of earlier writes in the same bundle. Supported patterns:
  • Conditional create: POST + entry.request.ifNoneExist
  • Conditional update: PUT {type}?{criteria} (optionally with If-None-Match)
  • Conditional patch: PATCH {type}?{criteria}
  • Conditional delete: DELETE {type}?{criteria}

Concurrency control (If-Match, If-None-Match)

TLQ FHIR enforces version checks where applicable:
  • If-Match: W/"{versionId}" is validated for PUT, PATCH, and DELETE.
  • Conditional PUT supports If-None-Match: * semantics (via the conditional update resolver).

PATCH in transactions (JSON Patch via Binary)

For transaction PATCH, TLQ FHIR expects the entry’s resource to be a Binary containing a JSON Patch document:
  • Binary.contentType = "application/json-patch+json"
  • Binary.data = base64-encoded JSON Patch bytes
Security/safety note: after applying the patch, TLQ FHIR removes resource.text (narrative) because it may no longer match the updated data. Example entry shape (data omitted for brevity):
{
  "request": { "method": "PATCH", "url": "Patient/123" },
  "resource": {
    "resourceType": "Binary",
    "contentType": "application/json-patch+json",
    "data": "BASE64_ENCODED_JSON_PATCH_BYTES"
  }
}

Version-specific references (resolve-as-version-specific)

TLQ FHIR implements the FHIR extension: http://hl7.org/fhir/StructureDefinition/resolve-as-version-specific If a Reference element has this extension and contains a versionless local reference ({type}/{id}), TLQ FHIR will (after all writes) rewrite it to a version-specific reference: {type}/{id}/_history/{versionId} …but only when the referenced target was written in the same transaction. The extension is removed as part of resolution.

Examples

Transaction: create + reference via fullUrl

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:patient-1",
      "request": { "method": "POST", "url": "Patient" },
      "resource": { "resourceType": "Patient", "name": [{ "family": "Doe" }] }
    },
    {
      "request": { "method": "POST", "url": "Observation" },
      "resource": {
        "resourceType": "Observation",
        "status": "final",
        "subject": { "reference": "urn:uuid:patient-1" }
      }
    }
  ]
}
TLQ FHIR will rewrite subject.reference to Patient/{uuid} before inserting the Observation.

Transaction: conditional create (ifNoneExist)

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:patient-1",
      "request": {
        "method": "POST",
        "url": "Patient",
        "ifNoneExist": "identifier=http://acme.example/mrn|123"
      },
      "resource": {
        "resourceType": "Patient",
        "identifier": [{ "system": "http://acme.example/mrn", "value": "123" }]
      }
    }
  ]
}
If a match exists, the entry returns 200 OK and fullUrl mapping points at the existing resource.