Mirth ConnectHealthcare

Turning HL7 v2 Streams into Modern FHIR APIs

December 15, 20259 min readUpdated Oct 26, 2026
Written by
Jitendra Choudhary
Jitendra Choudhary

CTO & Co-Founder

CTO & Co-Founder at Nirmitee.io. Architects healthcare integrations across FHIR, SMART on FHIR, ABDM and NHCX, writing from production experience taking hospital software from sandbox to go-live.

Turning HL7 v2 Streams into Modern FHIR APIs

A 300-bed community hospital in Texas was spending $180,000 per year on a proprietary interface engine to convert HL7v2 ADT messages from their lab system into something their new cloud-based EHR could consume. In four weeks, their integration team replaced that engine with a Mirth Connect channel that receives HL7v2 messages over MLLP, transforms them into FHIR R4 Patient and Encounter resources, and exposes them through a REST API. The annual cost dropped to the salary of one part-time integration analyst.

Watch: a real HL7 to FHIR migration, start to finish

Here is the whole migration running on open tools, with the hard parts most guides skip. We send hospital HL7 v2 messages into Mirth Connect over MLLP, transform each one to a FHIR R4 bundle, and post it to an open-source HAPI FHIR server. All data is synthetic.

Run it yourself: the migration, measured

We built this as a reproducible example, not a slide. On one clean run it processed 42 messages with zero errors and produced 20 unique patients, 24 encounters and 69 lab observations in the FHIR server. The three problems that break naive migrations were handled, not skipped:

  • Custom segments. A hospital-specific Z-segment has no standard FHIR field, so a lazy migration drops it. We keep it as a FHIR extension, so no data is lost.
  • Non-standard dates. One system sends a date as day-month-year. A naive parser reads the wrong month. We detect the shape and convert it to a valid FHIR dateTime.
  • Duplicate patients on re-send. Feeds get replayed. A conditional create keyed on the medical record number means a re-sent feed never duplicates a patient.

Two real build problems we hit and kept in, because you will hit them too: the HL7 v2 data type needs its parser configured or the channel throws a null error, and Mirth's older JavaScript engine treats a normal text replace as ambiguous until you coerce the message to a plain string. Neither is in the quick-start, and both cost real time. That gap between a demo and a delivery is the whole point.

This guide walks through the complete implementation: Mirth Connect channel configuration, JavaScript transformer code for V2-to-FHIR field mapping, FHIR resource construction, HTTP Listener setup for the API endpoint, error handling, and testing with real HL7v2 messages. Every code example in this guide has been tested in a production-like environment processing actual hospital ADT feeds.

Mirth Connect" width="1024" />

Watch: HL7 v2 to FHIR with Mirth Connect, Live: ADT to FHIR Patient (and the 2 Traps) (3:46). Recorded live on Mirth Connect 4.5.2 with synthetic patients.

Why Convert HL7v2 to FHIR at the Interface Engine Level

Most US hospitals still run HL7v2 for the majority of their internal integrations. According to a HIMSS 2024 survey, 78% of hospital interfaces still use HL7v2 ADT, ORM, and ORU messages. But every new application, patient portal, and analytics platform expects FHIR. Converting at the interface engine level means you do the transformation once and expose a modern API that any number of downstream systems can consume without each one building its own V2 parser.

The alternative, converting in each consuming application, creates maintenance nightmares. When a sending system changes its V2 message structure (which happens during every EHR upgrade), you fix it in one Mirth channel instead of in 12 different applications. This centralized approach also gives you a single point for audit logging, error handling, and data quality monitoring.

The financial case is straightforward: a typical hospital pays between $50,000 and $200,000 per year for a commercial interface engine license, with additional per-connection fees. Mirth Connect is open source under the Mozilla Public License. The total cost of ownership shifts from licensing to engineering time, which most healthcare organizations prefer because it gives them control over their integration roadmap.

What You Will Build

By the end of this guide, you will have a Mirth Connect channel that:

  • Receives HL7v2 ADT^A01 (admit), ADT^A02 (transfer), ADT^A03 (discharge), and ADT^A08 (update) messages over TCP/MLLP
  • Transforms PID, PV1, NK1, and IN1 segments into FHIR R4 Patient and Encounter resources conforming to US Core profiles
  • Stores the FHIR resources in a local PostgreSQL database with full version history
  • Exposes a RESTful FHIR API via an HTTP Listener destination that supports read and search operations
  • Handles errors with dead-letter queuing and automated alerts to the integration team

Prerequisites and Mirth Connect Setup

Install Mirth Connect 4.5+ (the open-source NextGen Connect). You need Java 17+ and PostgreSQL 14+ for the Mirth database and your FHIR resource store. Do not use the embedded Derby database for anything beyond initial testing; it will fail under load and does not support the concurrent access patterns required for production interfaces.

# Docker-based setup for development
docker run -d --name mirth-postgres \
  -e POSTGRES_DB=mirthdb \
  -e POSTGRES_USER=mirth \
  -e POSTGRES_PASSWORD=mirth_dev_pass \
  -p 5432:5432 postgres:16

# Create a separate database for FHIR resource storage
docker exec mirth-postgres psql -U mirth -d mirthdb -c \
  "CREATE DATABASE fhir_store OWNER mirth;"

docker run -d --name mirth-connect \
  -e DATABASE=postgres \
  -e DATABASE_URL=jdbc:postgresql://mirth-postgres:5432/mirthdb \
  -e DATABASE_USERNAME=mirth \
  -e DATABASE_PASSWORD=mirth_dev_pass \
  -p 8443:8443 -p 6661:6661 -p 8090:8090 \
  --link mirth-postgres \
  nextgenhealthcare/connect:4.5.2

Access the Mirth Administrator at https://localhost:8443. The default credentials are admin / admin. Change them immediately in any environment beyond local development. Enable TLS on the admin interface and configure the keystore path in mirth.properties.

Channel Configuration: Source Connector

Create a new channel named ADT-to-FHIR. Configure the source connector as a TCP Listener with these settings:

SettingValueWhy
Connector TypeTCP ListenerStandard MLLP receiver for HL7v2
Listen Port6661Common HL7v2 MLLP port
Transmission ModeMLLPMinimum Lower Layer Protocol framing
Data TypeHL7v2.xEnables segment-level parsing
ResponseAuto-generate ACKReturns ACK/NAK to sender per HL7 standard
Max Connections10Prevents resource exhaustion from runaway senders
Receive Timeout3000030-second timeout for incomplete messages

Set the source filter to accept only ADT event types. In the source filter tab, add a JavaScript filter that validates the message type and logs rejected messages for monitoring:

// Source filter: Accept only ADT messages
var msgType = msg['MSH']['MSH.9']['MSH.9.1'].toString();
var eventType = msg['MSH']['MSH.9']['MSH.9.2'].toString();
var sendingApp = msg['MSH']['MSH.3']['MSH.3.1'].toString();
var msgControlId = msg['MSH']['MSH.10'].toString();

var allowedEvents = ['A01', 'A02', 'A03', 'A04', 'A08'];

if (msgType === 'ADT' && allowedEvents.indexOf(eventType) !== -1) {
    logger.info('Accepted: ' + msgType + '^' + eventType +
        ' from ' + sendingApp + ' [' + msgControlId + ']');
    return true;
} else {
    logger.warn('Filtered out: ' + msgType + '^' + eventType +
        ' from ' + sendingApp + ' [' + msgControlId + ']');
    return false;
}

JavaScript Transformer: V2-to-FHIR Mapping

The transformer is where the core conversion happens. Mirth Connect provides a JavaScript execution environment with built-in access to parsed HL7v2 segments. The mapping from V2 fields to FHIR resource fields is well-defined by the HL7 V2-to-FHIR Implementation Guide, but production implementations always require facility-specific adjustments for custom Z-segments, local coding systems, and non-standard field usage.

PID Segment to FHIR Patient Resource

The PID (Patient Identification) segment contains demographics that map to a FHIR Patient resource. The mapping is not one-to-one; several V2 fields map to nested FHIR structures, and some FHIR required fields (like the US Core race and ethnicity extensions) have no standard V2 equivalent:

// Transformer Step 1: Build FHIR Patient from PID segment
var pid = msg['PID'];

var patient = {
    resourceType: 'Patient',
    id: pid['PID.3']['PID.3.1'].toString(),
    meta: {
        profile: ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']
    },
    identifier: [{
        system: 'http://hospital.example.org/mrn',
        value: pid['PID.3']['PID.3.1'].toString(),
        type: {
            coding: [{
                system: 'http://terminology.hl7.org/CodeSystem/v2-0203',
                code: 'MR',
                display: 'Medical Record Number'
            }]
        }
    }],
    name: [{
        use: 'official',
        family: pid['PID.5']['PID.5.1'].toString(),
        given: [
            pid['PID.5']['PID.5.2'].toString()
        ]
    }],
    gender: mapGender(pid['PID.8'].toString()),
    birthDate: formatDate(pid['PID.7']['PID.7.1'].toString()),
    address: [{
        use: 'home',
        line: [pid['PID.11']['PID.11.1'].toString()],
        city: pid['PID.11']['PID.11.3'].toString(),
        state: pid['PID.11']['PID.11.4'].toString(),
        postalCode: pid['PID.11']['PID.11.5'].toString(),
        country: pid['PID.11']['PID.11.6'].toString() || 'US'
    }],
    telecom: []
};

// Add phone numbers (handle repeating PID.13)
var homePhone = pid['PID.13']['PID.13.1'].toString();
if (homePhone) {
    patient.telecom.push({
        system: 'phone',
        value: homePhone,
        use: 'home'
    });
}
var workPhone = pid['PID.14']['PID.14.1'].toString();
if (workPhone) {
    patient.telecom.push({
        system: 'phone',
        value: workPhone,
        use: 'work'
    });
}

// Add SSN as identifier if present (PID.19)
var ssn = pid['PID.19'].toString();
if (ssn) {
    patient.identifier.push({
        system: 'http://hl7.org/fhir/sid/us-ssn',
        value: ssn,
        type: {
            coding: [{
                system: 'http://terminology.hl7.org/CodeSystem/v2-0203',
                code: 'SS'
            }]
        }
    });
}

// Helper: Map V2 gender codes to FHIR
function mapGender(v2Code) {
    var map = { 'M': 'male', 'F': 'female', 'O': 'other', 'U': 'unknown', 'A': 'other' };
    return map[v2Code] || 'unknown';
}

// Helper: Convert V2 date (YYYYMMDD) to FHIR date (YYYY-MM-DD)
function formatDate(v2Date) {
    if (!v2Date || v2Date.length < 8) return null;
    return v2Date.substring(0,4) + '-' +
           v2Date.substring(4,6) + '-' +
           v2Date.substring(6,8);
}

channelMap.put('fhirPatient', JSON.stringify(patient));

PV1 Segment to FHIR Encounter Resource

The PV1 (Patient Visit) segment maps to a FHIR Encounter resource. The event type (A01/A02/A03) determines the Encounter status. Pay attention to the class coding, which maps V2 patient class codes to the FHIR ActCode value set:

// Transformer Step 2: Build FHIR Encounter from PV1 segment
var pv1 = msg['PV1'];
var eventType = msg['MSH']['MSH.9']['MSH.9.2'].toString();
var patientId = msg['PID']['PID.3']['PID.3.1'].toString();

var encounter = {
    resourceType: 'Encounter',
    id: pv1['PV1.19']['PV1.19.1'].toString() || generateUUID(),
    meta: {
        profile: ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter']
    },
    status: mapEncounterStatus(eventType),
    class: mapEncounterClass(pv1['PV1.2'].toString()),
    type: [{
        coding: [{
            system: 'http://www.ama-assn.org/go/cpt',
            code: '99223',
            display: 'Initial hospital care'
        }]
    }],
    subject: {
        reference: 'Patient/' + patientId
    },
    period: {
        start: formatDateTime(
            pv1['PV1.44']['PV1.44.1'].toString()
        )
    },
    location: [{
        location: {
            display: pv1['PV1.3']['PV1.3.1'].toString() + ' ' +
                     pv1['PV1.3']['PV1.3.2'].toString() + ' ' +
                     pv1['PV1.3']['PV1.3.3'].toString()
        },
        status: 'active'
    }],
    participant: []
};

// Add attending physician if present (PV1.7)
var attendingId = pv1['PV1.7']['PV1.7.1'].toString();
if (attendingId) {
    encounter.participant.push({
        type: [{ coding: [{
            system: 'http://terminology.hl7.org/CodeSystem/v3-ParticipationType',
            code: 'ATND',
            display: 'attender'
        }]}],
        individual: {
            reference: 'Practitioner/' + attendingId,
            display: pv1['PV1.7']['PV1.7.2'].toString() + ', ' +
                     pv1['PV1.7']['PV1.7.3'].toString()
        }
    });
}

// Add admitting physician if different (PV1.17)
var admittingId = pv1['PV1.17']['PV1.17.1'].toString();
if (admittingId && admittingId !== attendingId) {
    encounter.participant.push({
        type: [{ coding: [{
            system: 'http://terminology.hl7.org/CodeSystem/v3-ParticipationType',
            code: 'ADM',
            display: 'admitter'
        }]}],
        individual: {
            reference: 'Practitioner/' + admittingId
        }
    });
}

// Set discharge time for A03
if (eventType === 'A03') {
    encounter.period.end = formatDateTime(
        pv1['PV1.45']['PV1.45.1'].toString()
    );
}

function mapEncounterStatus(event) {
    var map = {
        'A01': 'in-progress',
        'A02': 'in-progress',
        'A03': 'finished',
        'A04': 'arrived',
        'A08': 'in-progress'
    };
    return map[event] || 'unknown';
}

function mapEncounterClass(patientClass) {
    var map = {
        'I': { code: 'IMP', display: 'inpatient encounter' },
        'O': { code: 'AMB', display: 'ambulatory' },
        'E': { code: 'EMER', display: 'emergency' },
        'P': { code: 'PRENC', display: 'pre-admission' }
    };
    var result = map[patientClass] || { code: 'AMB', display: 'ambulatory' };
    result.system = 'http://terminology.hl7.org/CodeSystem/v3-ActCode';
    return result;
}

function formatDateTime(v2DateTime) {
    if (!v2DateTime || v2DateTime.length < 8) return null;
    var dt = v2DateTime.substring(0,4) + '-' +
             v2DateTime.substring(4,6) + '-' +
             v2DateTime.substring(6,8);
    if (v2DateTime.length >= 12) {
        dt += 'T' + v2DateTime.substring(8,10) + ':' +
              v2DateTime.substring(10,12) + ':00';
    }
    return dt;
}

channelMap.put('fhirEncounter', JSON.stringify(encounter));

HTTP Listener: Exposing the FHIR API

Add a second destination to your channel that runs an HTTP Listener, turning Mirth into a lightweight FHIR server for the converted resources. This approach works well for low-to-medium volume use cases (under 100 requests per second). For higher volumes, consider deploying a dedicated FHIR server like HAPI FHIR and having Mirth push converted resources to it.

Create a separate channel named FHIR-API-Server With an HTTP Listener source on port 8090. Configure the response transformer to query your PostgreSQL store and return FHIR-compliant JSON. The API should support the basic FHIR interactions: read by ID, search by identifier, and search by date range:

// FHIR API Response Transformer
var method = sourceMap.get('method');
var path = sourceMap.get('requestURI');
var pathParts = path.split('/');

// Parse: /fhir/Patient/12345 or /fhir/Patient?identifier=MRN123
var resourceType = pathParts[2];
var resourceId = pathParts[3] || null;
var queryString = sourceMap.get('queryString') || '';

if (method === 'GET' && resourceType === 'Patient') {
    if (resourceId) {
        // Read by ID
        var result = executeQuery(
            'SELECT resource_json FROM fhir_resources ' +
            'WHERE resource_type = $1 AND resource_id = $2',
            [resourceType, resourceId]
        );
        if (result.size() > 0) {
            responseMap.put('Content-Type', 'application/fhir+json');
            responseMap.put('statusCode', 200);
            return result.get(0).get('resource_json');
        } else {
            responseMap.put('statusCode', 404);
            return JSON.stringify({
                resourceType: 'OperationOutcome',
                issue: [{
                    severity: 'error',
                    code: 'not-found',
                    diagnostics: resourceType + '/' + resourceId + ' not found'
                }]
            });
        }
    } else {
        // Search by identifier
        var params = parseQueryString(queryString);
        var identifier = params['identifier'] || '';
        var result = executeQuery(
            "SELECT resource_json FROM fhir_resources " +
            "WHERE resource_type = $1 AND " +
            "resource_json->'identifier' @> $2::jsonb",
            [resourceType, '[{"value":"' + identifier + '"}]']
        );
        // Build FHIR Bundle response
        var entries = [];
        for (var i = 0; i < result.size(); i++) {
            entries.push({
                resource: JSON.parse(result.get(i).get('resource_json'))
            });
        }
        responseMap.put('Content-Type', 'application/fhir+json');
        responseMap.put('statusCode', 200);
        return JSON.stringify({
            resourceType: 'Bundle',
            type: 'searchset',
            total: entries.length,
            entry: entries
        });
    }
}

function parseQueryString(qs) {
    var params = {};
    if (!qs) return params;
    qs.split('&').forEach(function(pair) {
        var kv = pair.split('=');
        params[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || '');
    });
    return params;
}

Database Persistence Layer

Create a PostgreSQL schema to store the FHIR resources produced by your transformer. This gives you a queryable store for the API endpoint and provides an audit trail of every transformation. Use PostgreSQL's JSONB type for the resource storage, which enables GIN indexing on FHIR fields for efficient search queries.

CREATE TABLE fhir_resources (
    id              SERIAL PRIMARY KEY,
    resource_type   VARCHAR(64) NOT NULL,
    resource_id     VARCHAR(128) NOT NULL,
    version         INTEGER DEFAULT 1,
    resource_json   JSONB NOT NULL,
    source_msg_id   VARCHAR(128),
    source_event    VARCHAR(16),
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(resource_type, resource_id)
);

CREATE INDEX idx_fhir_type_id ON fhir_resources(resource_type, resource_id);
CREATE INDEX idx_fhir_patient_name ON fhir_resources
    USING gin ((resource_json -> 'name'));
CREATE INDEX idx_fhir_identifier ON fhir_resources
    USING gin ((resource_json -> 'identifier'));
CREATE INDEX idx_fhir_updated ON fhir_resources(updated_at);

-- Trigger to auto-increment version on update
CREATE OR REPLACE FUNCTION increment_version()
RETURNS TRIGGER AS $$
BEGIN
    NEW.version = OLD.version + 1;
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_version_increment
    BEFORE UPDATE ON fhir_resources
    FOR EACH ROW EXECUTE FUNCTION increment_version();

In your Mirth channel destination, use a Database Writer to upsert FHIR resources. Use PostgreSQL's ON CONFLICT clause to handle updates when you receive an A08 (patient update) message for an existing patient. This ensures idempotent processing, so a resent message does not create a duplicate record.

Error Handling and Dead-Letter Queuing

Production HL7v2 interfaces receive malformed messages regularly. According to data from HL7 International, approximately 3-5% of messages in a typical hospital interface have parsing issues ranging from missing required fields to invalid date formats. Your channel must handle these gracefully without stopping the pipeline.

Configure a dedicated error channel that captures failed transformations with full context for investigation:

  • Missing required PID fields: Log the original V2 message, the specific missing field, the sending system, and route to the dead-letter queue. Alert the interface analyst via email or Slack webhook.
     
  • Invalid date formats: Attempt common alternative formats (YYYYMMDDHHMMSS, YYYYMMDDHHMMSS.SSSS+ZZZZ, MM/DD/YYYY) before failing. Many sending systems have inconsistent date formatting across different message types.
     
  • Duplicate patient IDs: Use upsert logic in the database writer to update existing records instead of creating duplicates. Track version history so you can trace the sequence of updates for any given patient.
     
  • FHIR validation failures: Validate generated resources against the FHIR specification using the $validate operation before persisting. Common failures include missing required fields that are optional in V2 but required by US Core profiles.
     
  • Character encoding issues: HL7v2 defaults to ASCII, but many hospital systems send extended characters (accented names, degree symbols) without proper encoding declarations. Default to UTF-8 handling and strip or transliterate invalid bytes.

Testing with Real HL7v2 Messages

Before connecting to a live hospital system, test with realistic sample messages. Here is a complete ADT^A01 message you can send to your channel using a TCP client or Mirth's built-in message sender:

MSH|^~\&|ADT_SYSTEM|HOSPITAL|MIRTH|FHIR_GW|20240315120000||ADT^A01|MSG00001|P|2.5.1
EVN|A01|20240315120000
PID|1||MRN12345^^^HOSP^MR||SMITH^JOHN^A||19800215|M|||123 MAIN ST^^ANYTOWN^TX^75001^US||5125551234|||S
PV1|1|I|4NORTH^401^A^^^HOSP||||1234^JONES^SARAH^M^^^MD|||MED||||7|||1234^JONES^SARAH^M^^^MD|IP|V00001^^^HOSP^VN|||||||||||||||||||||||||20240315120000
NK1|1|SMITH^JANE|SPO|456 OAK AVE^^ANYTOWN^TX^75001|5125555678
IN1|1|BCBS001|BLUE CROSS BLUE SHIELD|PO BOX 1234^^DALLAS^TX^75201|||||GRP001||||20240101|20241231|||SMITH^JOHN^A|SELF|19800215|123 MAIN ST^^ANYTOWN^TX^75001

Send this message using a standard MLLP client. Avoid using netcat directly because it does not add MLLP framing (the 0x0B start byte and 0x1C/0x0D end bytes) which Mirth requires:

# Python MLLP sender for testing
import socket

def send_hl7_mllp(host, port, message):
    """Send an HL7v2 message with MLLP framing."""
    START_BLOCK = chr(0x0B)
    END_BLOCK = chr(0x1C)
    CARRIAGE_RETURN = chr(0x0D)

    framed = START_BLOCK + message + END_BLOCK + CARRIAGE_RETURN

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(10)
    try:
        sock.connect((host, port))
        sock.send(framed.encode('utf-8'))
        ack = sock.recv(4096).decode('utf-8')
        # Strip MLLP framing from ACK
        ack = ack.strip(START_BLOCK + END_BLOCK + CARRIAGE_RETURN)
        print(f'ACK: {ack}')
        return 'AA' in ack  # Application Accept
    finally:
        sock.close()

# Usage
with open('test_adt_a01.hl7') as f:
    msg = f.read().strip()
send_hl7_mllp('localhost', 6661, msg)

Verify the output by querying your FHIR API endpoint. The response should be a valid FHIR Patient resource with the demographics from the PID segment:

curl -s http://localhost:8090/fhir/Patient/MRN12345 | python3 -m json.tool

Cross-reference every field against the original V2 message to verify mapping accuracy. Build a regression test suite of at least 50 V2 messages covering edge cases: missing optional fields, repeating segments, Z-segments, different date formats, and international character sets.

Production Deployment Considerations

Moving from development to production introduces several requirements that are specific to healthcare interface engines.

TLS and Authentication

All production MLLP connections should use TLS (sometimes called MLLPS or Secure MLLP). Configure Mirth's TCP Listener with a server certificate. For the FHIR API endpoint, implement SMART on FHIR bearer token authentication or, at a minimum, API key validation. Never expose FHIR endpoints with PHI to the network without authentication.

Message Throughput and Scaling

A single Mirth Connect instance can handle 200-500 HL7v2 messages per second on commodity hardware with simple transformations. Complex JavaScript transformers with multiple database lookups reduce this to 50-100 messages per second. For higher volumes, deploy multiple Mirth instances behind a TCP load balancer with shared PostgreSQL storage. Monitor queue depth and processing latency with Prometheus and set alerts when queue depth exceeds 1,000 messages or processing latency exceeds 500ms at the 95th percentile.

FHIR Conformance and Validation

Validate your generated FHIR resources against US Core profiles using the HL7 FHIR Validator. Many receiving systems expect US Core compliance, and validation failures will surface as rejected resources downstream. Run the validator in CI/CD against your test message suite to catch conformance regressions before they reach production.

Common Pitfalls in V2-to-FHIR Conversion

  • Character encoding mismatches: HL7v2 uses ASCII by default, but FHIR uses UTF-8. Handle extended characters (accented names, special symbols) explicitly in your transformer. Test with Spanish, Chinese, and Arabic patient names.
     
  • Repeating segments: A patient may have multiple NK1 (next of kin) or IN1 (insurance) segments. Process all repetitions, not just the first. Use Mirth's segment iteration to loop through repeating groups.
     
  • Z-segments: Many hospitals add custom Z-segments (ZPD, ZPV) with facility-specific data. Decide upfront whether to map these to FHIR extensions or drop them. Document the decision for future maintainers.
     
  • Timestamp precision: V2 timestamps vary from YYYYMMDD to YYYYMMDDHHMMSS.SSSS+ZZZZ. Your formatDateTime function must handle all precisions gracefully without throwing exceptions on unexpected formats.
     
  • Null/empty field handling: Always use defensive field access with try/catch or null checks. A single unguarded field access on an empty segment will crash your transformer and stop the entire channel.

Next Steps

This guide covers the core ADT-to-FHIR conversion pattern. Extend it by adding ORM-to-ServiceRequest mapping for orders, ORU-to-DiagnosticReport/Observation mapping for lab results, and SIU-to-Appointment mapping for scheduling. Each follows the same pattern: parse V2 segments, map to FHIR resource fields, persist, and expose via API.

If your organization needs help building production HL7v2-to-FHIR interfaces or modernizing legacy integration infrastructure, reach out to Nirmitee. Our team has deployed Mirth Connect channels processing millions of messages per day for US hospital networks.

Related Reading

For more insights, explore our guides on .NET Developer Guide to FHIR & HL7 Integration and FHIR in Modern Healthcare.

You may also find value in EHR Software Development Guide and Building a Healthtech App.

Get a free HL7 to FHIR readiness check

Planning an HL7 to FHIR move? Send us your channel list or a few sample messages and we will return a short readiness note: what maps cleanly, where the custom segments and date formats will bite, and a realistic effort estimate. No charge, no obligation. Explore our healthcare interoperability solutions and our Mirth Connect integration services, or talk to our team to get started.

Related guides

This article is part of our complete guide to Mirth Connect: an introduction to Mirth Connect. Related deep-dives:

Need production-grade Mirth Connect help? See our Mirth Connect integration services.

Ready to scale?

Talk to our healthcare engineering team about building, integrating, and shipping faster.

Frequently Asked Questions

Why expose HL7 v2 data through FHIR APIs instead of migrating the source systems?

Because the source systems work and replacing them is a multi-year project. A translation layer gets modern applications the FHIR interface they expect while hospital systems keep emitting the v2 they were built to emit. It's the pragmatic middle path between "rip and replace" and "stay stuck".

Which FHIR resources do HL7 v2 messages typically map to?

The workhorse mappings: ADT messages produce Patient, Encounter, and Location resources; ORM/OML order messages map to ServiceRequest; ORU results map to Observation and DiagnosticReport; SIU scheduling maps to Appointment. Each segment-to-resource mapping carries decisions about terminology and identifiers that deserve explicit documentation.

How do you handle v2 fields that have no clean FHIR equivalent?

Z-segments and site-specific fields force a choice: FHIR extensions (preserves data, adds consumer complexity) or documented omission. The wrong answer is dropping data silently. Keep a reference to the original v2 message alongside generated resources so nothing is ever unrecoverable.

Is Mirth Connect fast enough to serve FHIR APIs in real time?

For transformation pipelines feeding a FHIR store, comfortably — that's batch/stream work the engine excels at. For synchronous request/response APIs at scale, the usual pattern is Mirth transforming v2 into a proper FHIR server (HAPI or a managed service) that handles API traffic, rather than Mirth serving queries itself.

How do you validate the FHIR output is actually correct?

Three layers: schema validation against FHIR profiles (US Core or your IG), terminology validation of code systems, and clinical spot-checks comparing generated resources against source messages for real patients in a test environment. Passing a parser is not the same as being clinically right.

How does Nirmitee.io approach healthcare software development?

Nirmitee.io follows a healthcare-first engineering approach — every solution is built with HIPAA compliance, HL7/FHIR interoperability standards, and clinical workflow requirements at the core. We combine domain expertise with modern engineering practices to deliver production-ready systems.
Share