04-clinicalwave: W2filled12 citations

Lab Results and Diagnostic Reports

Audiences: doctor, clinical-buyer, developer

Lab Results and Diagnostic Reports

Lab results in Dudoxx HMS are FHIR R4 DiagnosticReport resources stored in HAPI, indexed by LOINC code, and accessible through a REST API that supports per-patient filtering, full-text search on test names, and direct CRUD for clinic staff.

Business Purpose

Clinics generate lab results daily — blood panels, urine analyses, pathology reports, imaging reads. Before lab integration, doctors either re-entered results manually into their EMR or kept paper records separate from the clinical chart. Both patterns break the continuity of care: a doctor ordering a follow-up visit cannot quickly see whether the previous CBC result was in range.

Dudoxx HMS stores lab results as FHIR DiagnosticReport resources, which means:

  • Interoperability: Lab results are immediately shareable with specialist referral networks, insurance gateways, and patient portals using standard FHIR queries.
  • LOINC coding: Each result carries an optional LOINC code (testCode), enabling downstream analytics and decision-support tools to identify result types without string matching.
  • Patient-scoped access: Patients can view their own results via the patient portal (PATIENT role can GET /api/v1/lab-results/:id); staff can list all results for a clinic or filter by patient.

Audiences

  • Investor: Lab results are a high-frequency data type in any busy clinic. Having structured, FHIR-compliant lab data enables future integrations with automated lab ordering systems (HL7 v2 → FHIR bridge), clinical decision support, and payer portals — each of which represents a upsell or integration revenue stream.
  • Clinical buyer (doctor/nurse/receptionist): Doctors can record a lab result against a patient immediately after receiving it from the external lab. Results are listed chronologically on the patient chart. LOINC codes allow the system to surface abnormal patterns in the medical card's active conditions view.
  • Developer/partner: REST CRUD at GET|POST|PATCH|DELETE /api/v1/lab-results. Each result is identified by its FHIR DiagnosticReport numeric ID (not a UUID). Search supports patientId query param and full-text fuzzy match on testName. Requires X-Clinic-ID header. LOINC codes use system http://loinc.org.
  • Internal (ops/support): Lab results are pure FHIR — no Prisma table for LabResult. All data in HAPI. If a result is missing, check HAPI partition routing: the clinicSlug used in the write must match the X-Clinic-ID used in the read. Monitor HAPI at GET http://localhost:18080/fhir/metadata.

Architecture

diagram
LabResultsController   (/api/v1/lab-results)
  └── LabResultsService
        └── IFhirClient (FHIR_CLIENT)
              └── HAPI FHIR 8.4.0 (port 18080)
                    └── DiagnosticReport resources (partitioned by clinic)

LabResultsController metadata:
  @ResourceMeta operations: [list, get, create, update, delete, count, search]
  search.fields: [testName, testCode]
  search.fuzzySupported: true
  pagination.defaultSort: performedAt

The LabResultsController extends MetaMixin(MetaMixinBase), exposing a GET /lab-results/meta endpoint (RBAC-exempt) that returns field definitions, supported operations, and pagination config — used by the dynamic UI table renderer.

No ResourceFacade<R> is used here (lab results are Tier-3 FHIR-only, no fhir_resource_link bridge row). The service reads and writes FHIR directly through IFhirClient.

See coding_context/ddx-hms-context.md §Clinical Data for the module map.

Tech Stack & Choices

LayerTechnologyRationale
APINestJS 11, @Controller('lab-results')Uniform REST with Swagger auto-docs; ResourceMeta for dynamic UI
Auth@RequireRole(DOCTOR, NURSE, CLINIC_ADMIN, SUPER_ADMIN) for write; PATIENT allowed on GET :idPatients can view their own results; staff can view all
FHIR resourceDiagnosticReport (R4)Standard HL7 type for lab reports; carries LOINC coding, subject (Patient), and effective date
Coding systemLOINC (http://loinc.org)Universal lab test coding; required for external system interop
StorageHAPI FHIR 8.4.0 (pure FHIR, no Prisma mirror)Results never duplicated; single source of truth
FHIR clientIFhirClient (FHIR_CLIENT token)Partition-aware; maps HAPI 404/400 to NestJS exceptions
Observationsddx-api/src/clinical-data/observations/Separate module for FHIR Observation resources (individual test values within a DiagnosticReport)
Service requestsddx-api/src/clinical-data/service-requests/FHIR ServiceRequest (lab orders placed before results exist)

Key design decision: Lab results use numeric FHIR DiagnosticReport IDs (assigned by HAPI) as the external identifier — not UUIDs. This differs from Visit/Patient which use UUIDs. The controller's @ApiParam annotation explicitly documents 'FHIR DiagnosticReport ID' to avoid confusion.

Data Flow

Doctor records a lab result

Business outcome: A doctor receives a blood test result from the external lab and records it in the patient chart within 30 seconds.

Technical mechanism:

  1. POST /api/v1/lab-results with body { patientId, testName, testCode, status, performedAt } and X-Clinic-ID header.
  2. LabResultsController.create() (lab-results.controller.ts:73) delegates to LabResultsService.create().
  3. LabResultsService.create() (lab-results.service.ts:44) constructs a FhirDiagnosticReportResource:
    • code.coding[0] = { system: 'http://loinc.org', code: testCode, display: testName }
    • subject.reference = Patient/{patientId}
    • status = 'final' (default) or as specified
    • effectiveDateTime = performedAt
  4. fhirClient.create<FhirDiagnosticReportResource>('DiagnosticReport', resource, clinicSlug) POSTs to HAPI with partition header.
  5. HAPI assigns numeric ID; response is mapped to LabResultResponseDto and returned.

Doctor lists a patient's lab history

GET /api/v1/lab-results?patientId={uuid}LabResultsService.findAll(clinicSlug, patientId)fhirClient.search<FhirDiagnosticReportResource>('DiagnosticReport', { subject: 'Patient/{patientId}' }, clinicSlug) → mapped to LabResultResponseDto[].

Patient views own result

GET /api/v1/lab-results/:id with PATIENT role JWT → LabResultsService.findOne(id, clinicSlug)fhirClient.read<FhirDiagnosticReportResource>('DiagnosticReport', id, clinicSlug). Patient authorization is enforced by GatewayAuthGuard (JWT contains role); no additional patient-ownership check is implemented at the service layer — the partition routing ensures the result belongs to the patient's clinic.

Partial update (e.g., status correction)

PATCH /api/v1/lab-results/:idLabResultsService.update() (lab-results.service.ts:63) reads the existing resource, merges { status, performedAt, testName, testCode } fields, then calls fhirClient.update().

Implicated Code

  • ddx-api/src/clinical-data/lab-results/lab-results.controller.ts:33LabResultsController@ResourceMeta definition: operations, fields (patientId, testName, testCode, status, performedAt), pagination, search config
  • ddx-api/src/clinical-data/lab-results/lab-results.controller.ts:73create()POST /lab-results; delegates to service with ClinicId() context
  • ddx-api/src/clinical-data/lab-results/lab-results.controller.ts:83findAll()GET /lab-results?patientId=; optional patient filter
  • ddx-api/src/clinical-data/lab-results/lab-results.controller.ts:92findOne()GET /lab-results/:id; PATIENT role allowed
  • ddx-api/src/clinical-data/lab-results/lab-results.service.ts:24LabResultsService — injects FHIR_CLIENT; pure FHIR service (no Prisma)
  • ddx-api/src/clinical-data/lab-results/lab-results.service.ts:32findAll() — FHIR search with optional subject param
  • ddx-api/src/clinical-data/lab-results/lab-results.service.ts:44create() — builds FhirDiagnosticReportResource with LOINC code and Patient/ subject reference
  • ddx-api/src/clinical-data/lab-results/lab-results.service.ts:63update() — read-modify-write on HAPI DiagnosticReport
  • ddx-api/src/clinical-data/lab-results/lab-results.service.ts:80mapToResponse() — maps FHIR DiagnosticReport → LabResultResponseDto (extracts patientId from subject.reference)
  • ddx-api/src/clinical-data/observations/ — FHIR Observation resources (individual test values, e.g., a single CBC component)
  • ddx-api/src/clinical-data/diagnostic-reports/ — extended DiagnosticReport handling (radiology reports with attachments)
  • ddx-api/src/clinical-data/service-requests/ — FHIR ServiceRequest for lab order management (pre-result state)

Operational Notes

  • Numeric FHIR IDs, not UUIDs: Lab result IDs are HAPI-assigned numeric strings (e.g., "4912"). Do not pass UUIDs to GET /lab-results/:id — HAPI returns 404. This is the inverse of the Visit/Patient pattern which uses UUIDs as stable identifiers.
  • No Prisma mirror: Lab results have no labResult Prisma table. If a result appears in HAPI but not in the API response, check partition routing: the write clinicSlug must equal the read X-Clinic-ID. A slug mismatch silently returns empty results.
  • Status vocabulary: FHIR DiagnosticReport status values are: registered, partial, preliminary, final, amended, corrected, appended, cancelled, entered-in-error, unknown. The create() default is final; set registered for pending results.
  • LOINC is optional: The testCode field is not required. Results without a LOINC code are stored with code.text only and are not classifiable by downstream decision-support tools. Encourage LOINC codes for structured results.
  • PATIENT role access: Patients can read their own individual lab results (GET :id) but cannot list all results (GET /) — the list endpoint requires DOCTOR/NURSE/CLINIC_ADMIN/SUPER_ADMIN. Ownership enforcement relies on partition routing (patient's clinic matches the request clinic) not an explicit ownership check.
  • Fuzzy search: @ResourceMeta declares search.fuzzySupported: true on testName and testCode. The dynamic UI uses this to enable client-side fuzzy filtering — the API itself uses FHIR text search.
    Lab Results and Diagnostic Reports — Dudoxx Docs | Dudoxx