Moving Bahmni's Frontend to React and FHIR: A Guided Migration
Bahmni, an open-source hospital management system and EMR used primarily in low-resource settings, has a frontend that has aged past its original framework. Built nine years ago on AngularJS with an OpenMRS REST API, the system now faces the security and maintainability issues that come with a framework whose official support ended in December 2021. The modernization path we explored moves the frontend to React with TypeScript and data access to an HL7 FHIR API. Along the way, we tested a structured prompting workflow—Research, Review, Rebuild—that relies on Cline, Claude 3.5 Sonnet, an Atlassian MCP server, and a filesystem MCP server.
The results were significant. A task that would typically take three to six days of manual effort—migrating a single UI control—was completed in under an hour, at a cost of less than two dollars. This article describes the workflow, the tooling, and the technical steps involved in migrating one such control: the Treatments Display Control.
Why Bahmni's Frontend Needed Work
Bahmni's frontend is built on AngularJS, an early framework for dynamic web applications that has been deprecated for several years. The reliance on this outdated stack introduces real risks: unpatched dependencies create security vulnerabilities, onboarding developers unfamiliar with AngularJS is slow, and the codebase is increasingly incompatible with modern tools. For a healthcare system, these issues translate into potential harm to clinical workflows and patient data safety.
The original frontend is tightly coupled to the OpenMRS REST API, which in turn is bound to the OpenMRS data model and its implementation logic. OpenMRS is the open-source platform underlying Bahmni for clinical data modeling and patient record management. Bahmni's own development began in 2013, before FHIR—the modern healthcare data exchange standard—had reached normative status. FHIR Release 4 (R4), released in December 2018, was the first version with normative content, marking it as a stable, production-ready standard. Until recently, OpenMRS's FHIR support was limited and experimental. With that support now mature, a key goal of the migration is to architect the target system around FHIR R4, improving standardization and interoperability with external systems.
The Research, Review, Rebuild Workflow
The migration follows a three-phase cycle per feature: research, review, and rebuild. This keeps the process disciplined and iterative, with human validation at the critical checkpoint.
- Prepare a list of features targeted for migration. Select one feature to begin with.
- Use MCP servers to research the selected feature, generating a contextual analysis through a large language model.
- Have domain experts review the generated analysis to verify accuracy and alignment with existing project conventions and architectural guidelines. If the feature is not sufficiently isolated for migration, defer it and update the feature list.
- Proceed with LLM-assisted rebuild of the validated feature to the target framework.
- Repeat from step 2 until the list is empty.
Display Controls as Migration Targets
Bahmni includes over 30 display controls—modular, configurable widgets embedded across pages to display data like diagnoses, treatments, and lab results. Their decoupled nature makes them ideal candidates for targeted modernization. The control selected for this experiment is the Treatments Display Control, which presents a patient's drug orders over a configurable number of visits in a consolidated, tabular format. It supports both current and historical treatments, with built-in error handling, empty state management, and performance optimizations.
The control retrieves data from the /openmrs/ws/rest/v1/bahmnicore/drugOrders/prescribedAndActive endpoint, which returns a visitDrugOrders array. Each entry links a drug order to a specific visit and includes metadata about the provider, drug concept, and dosing instructions. Drug orders carry prescription details such as drug name, dosage, frequency, duration, administration route, start and stop dates, and standard code mappings like WHOATC, CIEL, SNOMED-CT, and RxNORM.
Here is a sample JSON response from the endpoint:
{
"visitDrugOrders": [
{
"visit": {
"uuid": "3145cef3-abfa-4287-889d-c61154428429",
"startDateTime": 1750033721000
},
"drugOrder": {
"concept": {
"uuid": "70116AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"name": "Acetaminophen",
"dataType": "N/A",
"shortName": "Acetaminophen",
"units": null,
"conceptClass": "Drug",
"hiNormal": null,
"lowNormal": null,
"set": false,
"mappings": [
{
"code": "70116",
"name": null,
"source": "CIEL"
},y
/* Response Truncated */
]
},
"instructions": null,
"uuid": "a8a2e7d6-50cf-4e3e-8693-98ff212eee1b",
"orderType": "Drug Order",
"accessionNumber": null,
"orderGroup": null,
"dateCreated": null,
"dateChanged": null,
"dateStopped": null,
"orderNumber": "ORD-1",
"careSetting": "OUTPATIENT",
"action": "NEW",
"commentToFulfiller": null,
"autoExpireDate": 1750206569000,
"urgency": null,
"previousOrderUuid": null,
"drug": {
"name": "Paracetamol 500 mg",
"uuid": "e8265115-66d3-459c-852e-b9963b2e38eb",
"form": "Tablet",
"strength": "500 mg"
},
"drugNonCoded": null,
"dosingInstructionType": "org.openmrs.module.bahmniemrapi.drugorder.dosinginstructions.FlexibleDosingInstructions",
"dosingInstructions": {
"dose": 1.0,
"doseUnits": "Tablet",
"route": "Oral",
"frequency": "Twice a day",
"asNeeded": false,
"administrationInstructions": "{\"instructions\":\"As directed\"}",
"quantity": 4.0,
"quantityUnits": "Tablet",
"numberOfRefills": null
},
"dateActivated": 1750033770000,
"scheduledDate": 1750033770000,
"effectiveStartDate": 1750033770000,
"effectiveStopDate": 1750206569000,
"orderReasonText": null,
"duration": 2,
"durationUnits": "Days",
"voided": false,
"voidReason": null,
"orderReasonConcept": null,
"sortWeight": null,
"conceptUuid": "70116AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
},
"provider": {
"uuid": "d7a67c17-5e07-11ef-8f7c-0242ac120002",
"name": "Super Man",
"encounterRoleUuid": null
},
"orderAttributes": null,
"retired": false,
"encounterUuid": "fe91544a-4b6b-4bb0-88de-2f9669f86a25",
"creatorName": "Super Man",
"orderReasonConcept": null,
"orderReasonText": null,
"dosingInstructionType": "org.openmrs.module.bahmniemrapi.drugorder.dosinginstructions.FlexibleDosingInstructions",
"previousOrderUuid": null,
"concept": {
"uuid": "70116AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"name": "Acetaminophen",
"dataType": "N/A",
"shortName": "Acetaminophen",
"units": null,
"conceptClass": "Drug",
"hiNormal": null,
"lowNormal": null,
"set": false,
"mappings": [
{
"code": "70116",
"name": null,
"source": "CIEL"
},
/* Response Truncated */
]
},
"sortWeight": null,
"uuid": "a8a2e7d6-50cf-4e3e-8693-98ff212eee1b",
"effectiveStartDate": 1750033770000,
"effectiveStopDate": 1750206569000,
"orderGroup": null,
"autoExpireDate": 1750206569000,
"scheduledDate": 1750033770000,
"dateStopped": null,
"instructions": null,
"dateActivated": 1750033770000,
"commentToFulfiller": null,
"orderNumber": "ORD-1",
"careSetting": "OUTPATIENT",
"orderType": "Drug Order",
"drug": {
"name": "Paracetamol 500 mg",
"uuid": "e8265115-66d3-459c-852e-b9963b2e38eb",
"form": "Tablet",
"strength": "500 mg"
},
"dosingInstructions": {
"dose": 1.0,
"doseUnits": "Tablet",
"route": "Oral",
"frequency": "Twice a day",
"asNeeded": false,
"administrationInstructions": "{\"instructions\":\"As directed\"}",
"quantity": 4.0,
"quantityUnits": "Tablet",
"numberOfRefills": null
},
"durationUnits": "Days",
"drugNonCoded": null,
"action": "NEW",
"duration": 2
}
]
}
Comparing the Data Models
The legacy API model and the OpenMRS FHIR MedicationRequest model differ in structure and intent. The Bahmni REST model is tailored for UI rendering, grouping orders by visit context and including OpenMRS-specific constructs like concept, drug, orderNumber, and flexible dosing instructions. The FHIR MedicationRequest model, by contrast, adheres to international standards with a normalized, reference-based structure. It uses resources such as Medication, Encounter, Practitioner, and represents coded elements through CodeableConcept and Timing.
Bridging these two models is the core technical challenge of the migration. The FHIR model is not a drop-in replacement; translating the view-oriented grouping and OpenMRS-specific fields into FHIR resources requires careful mapping. In the next step, the workflow's research phase is meant to document precisely these differences for each control, before any code is written.
Mapping Legacy Behavior
The research phase is where the migration actually starts taking shape. It centers on generating an MCP-augmented LLM analysis of the selected display control, examining the legacy source code and reverse engineering its behavior. This analysis is the foundation for the forward engineering work to come.
Not every requirement identified here will necessarily move forward. In long-lived systems, some functionality may be obsolete. But having a clear picture of what the system does today is critical for teams to decide what to retain, discard, or redesign in the target architecture.
The key is understanding how a human developer would approach this. Migrating from Angular to React relies heavily on contextual understanding across several dimensions:
- Purpose evaluation: the functional intent and role of the existing Angular components within the broader application.
- Data model analysis: reviewing underlying data structures and their relationships for compatibility with the new architecture.
- Data flow mapping: tracing how data moves from backend APIs to the frontend UI.
- FHIR model alignment: identifying whether the current data model maps to an HL7 FHIR-compatible structure.
- Comparative analysis: structural and functional similarities, differences, and gaps between old and target implementations.
- Performance considerations: areas for enhancement in the new system.
- Feature relevance: which features should carry forward, be redesigned, or be deprecated.
This context-driven analysis is often the hardest part of any legacy migration. Modernization is not just replacing outdated technology; it is reimagining the system's future across its entire lifecycle, including architecture, data structures, and user experience. Domain specialists and subject matter experts are crucial here.
Capturing Expected Behavior
A practical way to document expected behavior is through well-defined test scenarios against which the migrated code will be evaluated. These scenarios do double duty: they confirm that working functionality still works and new behavior functions as expected, and they give the LLM a clearly defined set of goals to meet. Explicit goals constrain the LLM's responses, making them as deterministic as possible and avoiding the unpredictability of probabilistic output.
To support this, I structured a comprehensive prompt that captures data flow, configuration, key functions, and integration points for the legacy component. Some parts of that prompt deserve particular attention:
- FHIR compatibility: mapping the custom Bahmni data model to HL7 FHIR resources to flag gaps and support future interoperability. This typically involves several hours of detailed analysis; with the LLM it's done in seconds.
- Testing guidelines: defined test scenarios for the React + TypeScript implementation, emphasizing data handling, rendering accuracy, and FHIR compliance. This gives the LLM a mandatory set of criteria to satisfy during rebuild.
- Customization options: documenting extension points and configuration mechanisms. LLM-generated analysis often surfaces customization paths embedded in the codebase that aren't documented, giving a more exhaustive picture of current capabilities.
For the actual data gathering, I used two lightweight servers: an Atlassian MCP server to extract available documentation on the display control, and a filesystem MCP server with the legacy frontend code and configuration mounted for source-level analysis. Both expose capabilities through the standard Model Context Protocol, which Cline uses as the client to access code, documentation, and configuration.
Since shipped configurations are opinionated and documentation is often outdated, I added explicit instructions to treat the source code as the single source of truth, with all other material as supplementary.
The Human Review
The review phase is where the human in the loop earns their keep. AI-generated analysis should not be accepted at face value, particularly for complex codebases. A domain expert and an architect still need to vet, contextualize, and guide the migration process. AI alone will not migrate an entire project seamlessly; it requires thoughtful decomposition, clear boundaries, and iterative validation.
In my case, I augmented the generated analysis with sample responses from the FHIR endpoint and discarded aspects not relevant to the modernization effort. Some capabilities are deferred for now—for instance, printing a prescription sheet based on prescribed medications. Performance optimizations, test cases not directly relevant to the migration, and configuration details like display row counts or active/inactive medication filters can all wait for the next iteration.
Review also means aligning the generated analysis with project conventions and practices. Without defined standards, every migration iteration risks producing non-conforming code, which over time accumulates into a fragmented codebase and technical debt. Project-specific style guides act as a foundational reference. For example, the Google TypeScript Style Guide can be summarized into a style guide file stored in the target codebase, and Cline reads this at the start of each session to keep generated TypeScript consistent.
Rebuilding Against Known Standards
With all required data gathered, the rebuild phase starts with a straightforward prompt: directing the LLM to build a Treatment display control in the new React/TypeScript FHIR frontend, pointing it at the legacy implementation doc and the display control guide. The LLM generates the initial code and test scenarios from that input.
What follows is a thorough code review by domain experts and developers, with refactoring applied as needed. Without this step, the output risks being a disorganized collection of code fragments rather than a cohesive system. Given the probabilistic nature of LLMs and the potential for discrepancies between generated code and original objectives, this review is essential to verify the migration was executed successfully.
Treat the review like that of a senior developer who has strong language and framework expertise but lacks project-specific context. Technical proficiency alone doesn't build robust systems; it takes domain nuance, architectural decisions, and long-term maintainability considerations. The human-in-the-loop brings the contextual awareness and system-level understanding that automated tools lack.
In our case, clearly defining intent and context up front minimized post-review refactoring. The requirements gathered during research—paired with explicitly stated project conventions, technology stack, coding standards, and style guides—left the LLM with little to infer on its own.
Still, unresolved questions about the implementation plan inevitably cause deviations. You cannot anticipate every "unknown unknown" in advance, and that is exactly where a detailed review matters. In this instance, my familiarity with the display control being rebuilt let me proactively minimize such unknowns, but that level of context is not always available. My recommendation stands: conduct detailed code reviews to uncover hidden gaps, and if recurring issues appear, refine the prompt to address them preemptively in future iterations.
LLMs are alluring because they seem to offer effortless solutions, and developers can produce working code quickly without deep coding experience. That allure should not bias experts into taking their hands off the wheel.
Results and Costs
The code generation itself completed in roughly 10 minutes. Using Claude 3.5 Sonnet (20241022), the full analysis and implementation—including unit and integration tests at approximately 95% coverage—cost about $2. Without AI support, the technical analysis and implementation would likely have consumed two to three days of a developer’s time.
Developing the reusable, general-purpose prompt took about five focused iterations over four hours, at an inference cost of around $10. This effort was essential because each display control in Bahmni is essentially a configurable, embeddable widget designed to enhance flexibility across different clinical dashboards. The shared architectural principles behind roughly 30 such controls made a modular, broadly applicable prompt feasible.
Even with AI assistance, the most significant cost remains the human time and cognitive load required to review and validate output. With prior Bahmni experience, reviewing the generated analysis took under 15 minutes, supplemented by quick parallel research to validate claims and data mappings. The quality of the analysis exceeded expectations: data model mapping was precise, transformation logic sound, and test suggestions covered both typical and edge cases comprehensively.
The Review Bottleneck
Code review proved to be the most significant challenge. A line-by-line review of all generated code took approximately 20 minutes. Unlike pairing with a human developer—where iterative discussions happen at a manageable pace—an AI that generates entire modules within seconds creates a bottleneck on the human side. This is not a limitation of the AI, but a reflection of human review capacity.
AI-assisted code reviewers can identify syntactic issues, adherence to best practices, and potential anti-patterns. However, they struggle to assess intent, which is critical in legacy migration projects. Domain context and business logic must still be confirmed by the human in the loop.
For a legacy modernization project migrating from AngularJS to React, this experience rates 10/10. The approach opens up the possibility for individuals with decent technical expertise and strong domain knowledge to migrate legacy codebases with minimal effort and in significantly less time.
Why Review Cannot Be Skipped
A well-structured plan and detailed scope review phase prior to implementation was a key enabler. Without a clear understanding of data flow, configuration structure, and display logic, the AI would struggle to produce coherent output.
In an earlier attempt where the review step was skipped, the generated code included a useMemo hook for a computationally trivial operation. The success criteria in the generated analysis specified that code should be performant, which the AI interpreted as requiring this unnecessary optimization—complete with unit tests validating the performance of that operation. None of this was explicitly required; it arose solely from poorly defined intent.
Review also prevents wasteful back-and-forth during the rebuild. While refining the prompt for the Display Control Implementation Guide, one iteration omitted the section specifying unit tests. The result was a largely meaningless test offering a false sense of coverage with no real connection to the code under test.
Attempting to fix that test through extensive prompting only compounded the problem. The more prompts were added—examples, detailed instructions on test structure—the further the process deviated from rebuilding the display control. The focus shifted entirely to unit test issues, with the AI even reviewing unrelated tests and suggesting fixes. Restarting with clearly defined instructions proved far more effective.
This leads to a crucial insight: Don't Interrupt AI. LLMs are predictive sequence generators that build narratives token by token. Interrupting mid-stream to course-correct breaks the logical flow being constructed. Stanford's "Lost in the Middle" study found models can suffer up to a 20% drop in accuracy when critical information is buried in the middle of long contexts versus clearly framed upfront. Starting with a well-defined prompt and letting the AI complete its task unimpeded yields better results than constant backtracking.
As Nick Baumann argues in "Why Human Intent Matters More as AI Capabilities Grow," clear human intent—not just brute model strength—becomes the key to unlocking useful output. Practitioners benefit most by designing clean, unambiguous setups and letting the AI complete the arc without interruption.
Documentation as a Byproduct
Modernizing the legacy codebase is the main product, but not the only valuable one. The generated documentation about the system serves end users and implementers by complementing or filling gaps in existing system documentation. It also stands as a knowledge base for forward engineering teams pairing with LLMs to enhance system capabilities.
The combination of a bottom-up approach—breaking the problem into atomic components—with clearly defined best practices and guidelines produces a crisp enough document to use limited context window space efficiently. This allows the LLM to analyze code without being limited by how the code was originally organized by developers. It also reduces cost: a brute-force approach would spend ten times as much even for a simpler project.
Conclusion
This approach is not a silver bullet for large-scale migration without oversight. Its strength is in reducing development time—potentially by several weeks—while maintaining quality and control. The goal is to amplify human expertise, not replace it, accelerating delivery while preserving or improving maintainability.
The experience is limited to read-only controls. More complex or interactive components may present additional challenges requiring further evaluation and refinement of prompts.
Moravec's Paradox aptly illustrates a key insight: tasks perceived as intellectually complex, such as logical reasoning, are relatively easier for AI, while tasks requiring human intuition and contextual understanding remain challenging. This reinforces the importance of subject matter experts whose deep experience enables accurate interpretation of requirements, validation of AI-generated outputs, and informed decision-making—ensuring transformation aligns with organizational goals and constraints.
While project-specific complexities may render this approach ambitious, the structured workflow offers a strong starting point. It reduces manual overhead, creates forward momentum, and lays groundwork for cleaner implementations through expert-led, guided refactoring. The quality and architectural soundness of the legacy system remain critical factors, but this methodology opens the possibility for any individuals with decent technical expertise and strong domain knowledge to migrate legacy codebases with minimal effort.



