@healthcare-interoperability/fhir-response-builders
v1.1.0
Published
Shape FHIR R4 REST responses (OperationOutcome / Parameters / Bundle) from WriteResult objects produced by @healthcare-interoperability/fhir-storage-core.
Downloads
318
Readme
@healthcare-interoperability/fhir-response-builders
Turn WriteResult objects (from @healthcare-interoperability/fhir-storage-core) into FHIR R4 REST responses — OperationOutcome, Parameters, or Bundle of type transaction-response.
Zero runtime dependencies. ESM. Node.js ≥ 20.
Install
npm install @healthcare-interoperability/fhir-response-buildersUsage
import {
FHIRResponseBuilder,
FHIROperationOutcomeBuilder,
FHIRParametersBuilder,
FHIRBundleResponseBuilder,
} from '@healthcare-interoperability/fhir-response-builders';
const responses = new FHIRResponseBuilder();
// After a write:
const writeResult = await repo.upsert(resource, integrationConfig);
// Pick whichever response shape fits your endpoint:
res.json(responses.bundle(writeResult));
// or
res.json(responses.parameters(writeResult));
// or
res.json(responses.operationOutcome(writeResult));Multi-WriteResult (Bundle ingestion)
Each method accepts a single WriteResult or an array of them:
const results = await Promise.all([
patientRepo.bulkUpsert(patients, integrationConfig),
observationRepo.bulkUpsert(observations, integrationConfig),
]);
res.json(responses.bundle(results));The output Bundle.entry[] (or Parameters.parameter[], or OperationOutcome.issue[]) flattens across all WriteResults in input order.
Note on order preservation for true FHIR transaction-response semantics: these builders preserve the order they receive WriteResults and items. If your input is a FHIR transaction Bundle where
response.entry[i]must correspond torequest.entry[i], the ingest layer must order the input WriteResults to match.
Builders
FHIROperationOutcomeBuilder
const b = new FHIROperationOutcomeBuilder();
b.build(writeResult); // one issue per stored doc
b.build(writeResult, { summary: true }); // single aggregate issueOutput (per-item):
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "informational",
"diagnostics": "Patient/pt-1 created as Patient/hash_xyz"
}
]
}Output (summary):
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "information",
"code": "informational",
"diagnostics": "Processed 500 resources across 3 types: 480 created, 20 updated. By type: Patient(200), Observation(250), Encounter(50)"
}
]
}Override _buildDiagnostics, _buildIssue, or _buildSummary to customize.
FHIRParametersBuilder
const b = new FHIRParametersBuilder();
b.build(writeResult);Output:
{
"resourceType": "Parameters",
"parameter": [
{
"name": "outcome",
"part": [
{ "name": "resourceType", "valueString": "Patient" },
{ "name": "fhirId", "valueString": "pt-1" },
{ "name": "_id", "valueString": "hash_xyz" },
{ "name": "action", "valueString": "created" }
]
}
]
}In customer mode, each parameter additionally carries a customerId part.
Override _buildParts or _buildParameter to change the shape.
FHIRBundleResponseBuilder
const b = new FHIRBundleResponseBuilder();
b.build(writeResult, {
baseUrl: 'https://api.example.com/fhir', // optional, defaults to relative URLs
bundleId: 'tx-1', // optional, must match FHIR id pattern
includeTotal: true, // optional, sets Bundle.total
addCustomerId: true, // customer-mode only
customerIdExtensionUrl: 'https://acme.com/fhir/StructureDefinition/customerId',
});Output:
{
"resourceType": "Bundle",
"id": "tx-1",
"type": "transaction-response",
"total": 1,
"entry": [
{
"fullUrl": "https://api.example.com/fhir/Patient/hash_xyz",
"response": {
"status": "201 Created",
"location": "https://api.example.com/fhir/Patient/hash_xyz"
}
}
]
}With addCustomerId: true and customerIdExtensionUrl set, customer-mode fan-out entries get an extra extension[] array carrying the customerId. The extension URL is required — devs choose a URL under their own FHIR namespace.
Override _buildEntry, _buildLocation, _buildResponse, or _statusFromAction to customize.
Limitations
Bundle.entry.response.etagis omitted. ETags requiremeta.versionId, which isn't exposed onWriteResult.items(yet). Subclass_buildResponseand addetagonce you have a way to fetch the version per_id.Bundle.totalis canonical forsearchsetBundles per the FHIR spec. Fortransaction-responsethe spec leaves it undefined — this builder treats it as the entry count whenincludeTotal: true.
FHIRResponseBuilder (orchestrator)
Combines all three into one entry point. Swap individual builders to customize one shape:
class MyBundleBuilder extends FHIRBundleResponseBuilder {
_buildLocation(ctx) {
return `https://my.app/fhir/${ctx.resourceType}/${ctx.item._id}`;
}
}
const responses = new FHIRResponseBuilder({
bundleBuilder: new MyBundleBuilder(),
});
res.json(responses.bundle(writeResult)); // uses custom bundle builder
res.json(responses.parameters(writeResult)); // default Parameters builderWriteResult shape
These builders expect the shape produced by fhir-storage-core:
interface WriteResult {
items: Array<{
originalIndex: number;
fhirId: string;
_id: string;
customerId: string | null;
action: 'created' | 'updated' | 'noop' | 'resurrected' | 'written';
}>;
counts: { created: number; modified: number; matched: number };
dedupMode: 'integration' | 'customer' | 'parent';
resourceType: string;
}You can also construct WriteResult objects yourself for callers that don't use fhir-storage-core. The builders validate shape at build() time and throw clearly on malformed input.
Requirements
- Node.js ≥ 20
@healthcare-interoperability/fhir-storage-core(recommended, but not required — the builders only need theWriteResultshape)
License
MIT
