Why I Put Mirth Connect in Front of FastAPI Instead of Parsing HL7 in Python

Why I Put Mirth Connect in Front of FastAPI Instead of Parsing HL7 in Python

为什么我选择在 FastAPI 前面部署 Mirth Connect,而不是直接用 Python 解析 HL7

When I started building my Maternity HL7-to-FHIR Pipeline, my first instinct was to do everything in Python. Parse the HL7 message, map the fields, validate the FHIR resource, persist it, all in one FastAPI service. It was clean. It was simple. It was wrong.

当我开始构建“产科 HL7 转 FHIR”流水线时,我的第一直觉是全部用 Python 完成。解析 HL7 消息、映射字段、验证 FHIR 资源、持久化存储,所有工作都在一个 FastAPI 服务中完成。这看起来很整洁,也很简单,但实际上是错误的。

Some Background

背景介绍

I’ve been building an open-source pipeline that takes HL7 v2.5 messages, the kind hospital maternity systems still send over raw TCP connections, and turns them into FHIR R4 resources with Australian Base profiles. Patient admissions, lab orders, vital signs. The sort of data that’s been flowing between hospital systems in pipe-delimited text since the 1990s, now mapped into modern healthcare APIs.

我一直在构建一个开源流水线,它接收 HL7 v2.5 消息(即医院产科系统至今仍通过原始 TCP 连接发送的那种消息),并将其转换为带有澳大利亚基础配置文件的 FHIR R4 资源。这些数据包括患者入院记录、检验医嘱和生命体征。自 20 世纪 90 年代以来,这类数据一直以管道符分隔的文本形式在医院系统间传输,而现在我将其映射到现代医疗 API 中。

If you’re curious about the full project, I wrote a detailed walkthrough in Bridging Legacy Hospital Messages to Modern Healthcare APIs. That article covers the end-to-end architecture, but here’s the short version:

如果你对整个项目感兴趣,我写过一篇详细的指南:《将遗留医院消息桥接到现代医疗 API》。那篇文章涵盖了端到端的架构,以下是简要版本:

  • Hospital Maternity System

  • HL7 v2.5 messages (ADT, ORM, ORU) over MLLP (raw TCP, port 6661)

  • Mirth Connect 4.5 (Integration Engine): Receives MLLP, parses HL7, extracts fields, routes by message type

  • FastAPI + Python 3.11 (Transformation Layer): Clean JSON over HTTP, Pydantic validation, FHIR R4 mapping, AU Base profiles

  • HAPI FHIR Server 7.0.3: FHIR R4 resources (conditional PUT/POST), Persistence + FHIR API

  • 医院产科系统

  • 通过 MLLP(原始 TCP,端口 6661)发送的 HL7 v2.5 消息(ADT, ORM, ORU)

  • Mirth Connect 4.5(集成引擎): 接收 MLLP,解析 HL7,提取字段,按消息类型路由

  • FastAPI + Python 3.11(转换层): 通过 HTTP 传输纯净的 JSON,进行 Pydantic 验证、FHIR R4 映射及 AU 基础配置

  • HAPI FHIR Server 7.0.3: FHIR R4 资源(条件 PUT/POST),负责持久化和 FHIR API

Three containers, one docker compose up. Mirth handles the legacy protocol world, FastAPI handles the FHIR world, HAPI stores everything. But this article isn’t about the what. It’s about one particular why: why the pipeline has two services doing the transformation work instead of one.

三个容器,一个 docker compose up 即可启动。Mirth 处理遗留协议世界,FastAPI 处理 FHIR 世界,HAPI 负责存储。但本文不谈“是什么”,而是谈一个特定的“为什么”:为什么流水线需要两个服务来完成转换工作,而不是一个。

The “Just Parse It in Python” Phase

“直接用 Python 解析”阶段

My initial architecture looked like this: Hospital System --MLLP--> Python Script --> HAPI FHIR Server. I used python-hl7 to split messages on | and count field positions. For a single ADT^A01 (patient admission) message, it worked fine. I could pull the patient name from PID-5, the MRN from PID-3, the gender from PID-8, and build a FHIR Patient resource from it. Then I tried a real-ish maternity workflow (an admission, an order, and a set of vitals) and things fell apart quickly.

我最初的架构是:医院系统 --MLLP--> Python 脚本 --> HAPI FHIR 服务器。我使用 python-hl7 通过 | 分割消息并计算字段位置。对于单个 ADT^A01(患者入院)消息,这运行良好。我可以从 PID-5 获取患者姓名,从 PID-3 获取病历号 (MRN),从 PID-8 获取性别,并据此构建 FHIR 患者资源。但当我尝试一个真实的产科工作流(入院、医嘱和一组生命体征)时,一切很快就崩溃了。

Five Problems That Changed My Mind

让我改变主意的五个问题

1. MLLP Is Not HTTP

1. MLLP 不是 HTTP

Hospital systems don’t send HL7 over HTTP. They send it over MLLP (Minimum Lower Layer Protocol), which is a TCP socket protocol with specific framing bytes (\x0b at the start, \x1c\x0d at the end). The sender expects an ACK or NACK response in HL7 format, not an HTTP status code. Building an MLLP listener in Python is possible. Libraries like aioml7 exist. But you’re now maintaining a custom TCP server alongside your HTTP API server, handling connection pooling, timeouts, and HL7 acknowledgment generation. That’s a lot of infrastructure code that has nothing to do with your actual transformation logic. Mirth Connect handles MLLP natively. You point it at a port, it listens, it parses, it ACKs. Done. One config screen, no custom code.

医院系统不会通过 HTTP 发送 HL7,而是通过 MLLP(最小底层协议)。这是一种 TCP 套接字协议,带有特定的帧字节(开头为 \x0b,结尾为 \x1c\x0d)。发送方期望的是 HL7 格式的 ACK 或 NACK 响应,而不是 HTTP 状态码。用 Python 构建 MLLP 监听器是可能的,也有像 aioml7 这样的库。但这意味着你需要在 HTTP API 服务器之外额外维护一个自定义 TCP 服务器,处理连接池、超时和 HL7 确认生成。这些基础设施代码与你的实际转换逻辑毫无关系。Mirth Connect 原生支持 MLLP。你只需指定端口,它就会监听、解析并自动回复 ACK。搞定。一个配置界面,无需自定义代码。

2. HL7 Parsing Is Messier Than It Looks

2. HL7 解析比看起来更混乱

The pipe-delimited format looks simple: PID|1||1234567^^^MRN||TEST^PATIENT^MARY^^MS||19920315|F|||14 SAMPLE ST^^SYDNEY^NSW^2000^AU. But consider:

  • Component separators: PID-5 is TEST^PATIENT^MARY^^MS, which is family, given, middle, suffix (empty), prefix. Miss the empty suffix and your prefix ends up as the suffix.
  • Repeating fields: PID-3 can contain multiple identifiers separated by ~. You need to iterate and match by identifier type.
  • Escape characters: A patient named O’Brien might appear as O\T\Brien.
  • Encoding characters: Different hospitals can (and do) use different separators.

管道符分隔格式看起来很简单:PID|1||1234567^^^MRN||TEST^PATIENT^MARY^^MS||19920315|F|||14 SAMPLE ST^^SYDNEY^NSW^2000^AU。但请考虑:

  • 组件分隔符: PID-5 是 TEST^PATIENT^MARY^^MS,代表姓、名、中间名、后缀(空)、前缀。如果漏掉空的后缀,你的前缀就会被误认为是后缀。
  • 重复字段: PID-3 可能包含多个由 ~ 分隔的标识符。你需要遍历并按标识符类型进行匹配。
  • 转义字符: 名为 O’Brien 的患者在 HL7 中可能显示为 O\T\Brien
  • 编码字符: 不同的医院可以使用(并且确实在使用)不同的分隔符。

In my Python script, I was doing segment.split('|')[5].split('^')[0] to get a family name. One unexpected empty field, one unexpected repeating group, and the whole positional mapping shifted silently. No error, just wrong data in the FHIR resource. Mirth Connect’s HL7 parser handles all of this natively. In a Mirth transformer, I write: var familyName = msg['PID']['PID.5']['PID.5.1'].toString();. That’s an E4X/XML path against a parsed HL7 tree, not string splitting. It handles repeating fields, component separators, and encoding characters correctly because that’s what the parser is built for. Years of edge cases baked into a mature parser versus my three-day-old string splitter.

在我的 Python 脚本中,我曾使用 segment.split('|')[5].split('^')[0] 来获取姓氏。只要出现一个意外的空字段或重复组,整个位置映射就会悄无声息地偏移。没有报错,但 FHIR 资源中的数据就是错的。Mirth Connect 的 HL7 解析器原生处理了所有这些情况。在 Mirth 转换器中,我只需写:var familyName = msg['PID']['PID.5']['PID.5.1'].toString();。这是针对已解析 HL7 树的 E4X/XML 路径,而不是字符串分割。它能正确处理重复字段、组件分隔符和编码字符,因为这就是解析器的设计初衷。这是经过多年边缘情况考验的成熟解析器,而我那只是个用了三天的字符串分割脚本。

3. Message Routing Is a Separate Concern

3. 消息路由是一个独立的问题

A maternity workflow involves three message types: ADT^A01 (Patient admitted), ORM^O01 (Order placed), ORU^R01 (Results available). In the “everything in Python” design, my MLLP listener would receive a raw HL7 message, I’d parse MSH-9 to determine the message type, then route to the right handler function. That’s a message router, and I’d be building one from scratch. Mirth Connect is literally a message router. You define a channel per message type, point each destination at a different FastAPI endpoint, and Mirth handles the dispatch. The FastAPI endpoints receive clean, typed JSON payloads. They don’t even need to know HL7 exists.

产科工作流涉及三种消息类型:ADT^A01(患者入院)、ORM^O01(医嘱下达)、ORU^R01(结果可用)。在“全部用 Python”的设计中,我的 MLLP 监听器会接收原始 HL7 消息,我需要解析 MSH-9 来确定消息类型,然后路由到正确的处理函数。这本质上是一个消息路由器,而我是在从零开始构建它。Mirth Connect 本身就是一个消息路由器。你可以为每种消息类型定义一个通道,将每个目的地指向不同的 FastAPI 端点,Mirth 会负责分发。FastAPI 端点接收的是干净的、类型化的 JSON 负载,它们甚至不需要知道 HL7 的存在。