Rewrite Legacy Code Without Breaking It: Test-First
Most rewrites port the screens and hope behavior matches. We did the opposite: rebuilt the engine test-first, to 340 tests, then wrapped it in a new UI.
Summarize with:

Introduction
Most rewrites start with the screens. You rebuild the pages in the new framework, click around, and hope the behavior matches the old version. We did the opposite. We rebuilt the engine first, proved it with tests, and only then put a new interface on top.
This is the story of how we rewrote one of our trickiest pieces of software, a recorder that captures audio and video in the browser, by treating the logic and the interface as two separate jobs. I am writing it because the order we did things in was the whole reason it worked, and it is something you can copy.
What you'll learn:
- Why a recorder is one of the hardest things to rewrite
- What "engine first" means, and why we tested the core before building any UI
- How separating the engine from the interface made the new version safe to ship
- An honest account of what still broke after all that testing

Why a recorder is hard to rewrite
A recorder looks simple from the outside. You press a button, it captures audio and video, you stop, and you have a recording. Underneath, it is full of edge cases. Different browsers handle media in different ways. Phones behave differently from laptops. The camera has to warm up. Permissions can be denied. A recording can be interrupted. Years of these small cases were baked into the old version, and most of them were invisible until they broke.
That is the real risk in rewriting something like this. The happy path is easy to rebuild. The hundreds of small behaviors around it are what take the time, and they are exactly the parts you forget when you are focused on making the screens look right.
What "engine first" means
So we split the work in two. First, the engine. We pulled the actual recording logic into a separate, headless piece, which means it has no buttons or screens of its own. It just captures media and reports what is happening. All the hard behavior lives here: starting and stopping, handling each browser, warming up the camera, dealing with denied permissions.
Then, separately, the interface. The new screens, built in React and Vite, are a thin layer that drives the engine and shows what it is doing. The interface does not contain the hard logic. It asks the engine to do things and displays the result.
Keeping these apart matters because it lets you prove the hard part on its own, before you have built a single screen.
Testing the core before the UI
This is the part I am most glad we did. We wrote tests for the engine as we built it, and we kept going until the engine had grown to 340 tests. Each test pins down one of those small behaviors, so a browser quirk or an interrupted recording is checked automatically rather than found by a user.
We did this before the new interface existed. By the time we started on the screens, the engine was already proven. The screens became the easy part, because all the risk had been handled underneath them and locked down by tests. If you want the wider story of how we run rewrites while staying live, I wrote about it in rewriting a live web app from Angular to React in 7 weeks.
The lesson is simple. Put your tests where the risk is. For a recorder, the risk is in the engine, not the buttons, so that is where the tests went.
What still broke
I want to be honest, because a clean story is a dishonest one. Even with the engine tested to 340 tests, the move was not perfect. When we put the new version next to the old one in real conditions, we found a handful of parity problems, things that worked slightly differently from the old version, including a mobile capture issue and a storage path detail. We fixed them in a focused round before shipping.
That does not undercut the approach. It supports it. The problems we found were specific and small, the kind you can fix in a focused pass, instead of a vague "recording feels broken" that you cannot pin down. Tests do not remove every bug. They shrink the bugs that remain into things you can name and fix.
What I would tell you
- For anything with hard logic, separate the engine from the interface. The engine holds the behavior, the interface just drives it.
- Put your tests where the risk is. Rebuild and prove the risky core before you build the screens.
- Expect a parity pass anyway. Even a well-tested rewrite needs a round of comparing old and new in real conditions.
- A clean rewrite story is usually missing the honest part. The value of tests is not zero bugs, it is bugs you can name.
Frequently Asked Questions
Further Reading
Tags
Related Articles
Try Our Free Tools
AI Video Prompt Generator
Generate production-ready AI video prompts through conversation. Optimized for Sora 2 and Gemini video generation
AI Video Analyzer
Analyze video content frame-by-frame with AI. Content moderation, security monitoring, accessibility, and product demos
Text Language Detector & Translator
Detect any language and translate text instantly with browser-based AI