---
title: "Rewrite Legacy Code Without Breaking It: Test-First"
date: 2026-06-28T00:00:00.000Z
description: "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."
tags: [software rewrite, testing, test-driven development, React, Vite, frontend architecture, engineering discipline, build in public, migration, code quality]
canonical: https://vatsalshah.ca/blog/engine-first-migration-tests-before-ui
---
## 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

![UI-first versus engine-first: rebuild and test the core before building any screens](/images/blog/engine-first-migration.png)

## 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](/blog/the-honest-pivot-nextjs-to-react-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](/blog/rewriting-angular-to-react-with-ai-agents).

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.

<FAQSection
  title="Frequently Asked Questions"
  questions={[
    {
      question: "What does engine first mean?",
      answer:
        "It means rebuilding the core logic of a feature as a separate, headless piece with no screens, then putting a thin interface on top. The engine holds the hard behavior, and the interface just drives it and shows the result.",
    },
    {
      question: "Why is a recorder hard to rewrite?",
      answer:
        "The happy path is easy, but a recorder has hundreds of small edge cases. Different browsers handle media differently, phones differ from laptops, the camera has to warm up, permissions can be denied, and a recording can be interrupted. Those small behaviors are what take the time.",
    },
    {
      question: "Why test the engine before building the UI?",
      answer:
        "Because the risk lives in the engine, not the screens. By proving the engine with tests first, the hard part is locked down before any interface exists, which makes the screens the easy part. We kept writing tests until the engine had grown to 340.",
    },
    {
      question: "Did testing the engine mean the rewrite had no bugs?",
      answer:
        "No. When we compared the new version to the old one in real conditions, we found a handful of parity problems, including a mobile capture issue, and fixed them in a focused round. Tests do not remove every bug. They shrink the remaining bugs into specific things you can name and fix.",
    },
    {
      question: "How do you separate an engine from an interface?",
      answer:
        "You move all the logic that does the real work into a headless module that exposes a clear set of actions and reports its state. The interface then calls those actions and renders the state, without containing the logic itself. This lets you test the engine on its own.",
    },
    {
      question: "When is engine first worth it?",
      answer:
        "When the feature has hard, edge-case-heavy logic, like recording, media handling, or anything stateful and browser-dependent. For a simple form or a static page, the overhead is not worth it. The harder the logic, the more engine first pays off.",
    },
    {
      question: "What framework did the new recorder use?",
      answer:
        "The new interface is built in React with Vite. React builds the screens and Vite builds and serves the code. The recording engine itself is framework-agnostic, which is part of why it could be tested on its own.",
    },
    {
      question: "What is the main takeaway?",
      answer:
        "Put your tests where the risk is, and prove the risky core before you build the screens. Separating the engine from the interface is what makes that possible, and it turns a scary rewrite into a tested core plus a thin, easy UI.",
    },
  ]}
/>

## Further Reading

- [Rewriting a live web app from Angular to React in 7 weeks](/blog/rewriting-angular-to-react-with-ai-agents)
- [From prompts to an AI engineering team](/blog/from-prompts-to-an-ai-engineering-team)
- [The honest pivot: Next.js to React + Vite](/blog/the-honest-pivot-nextjs-to-react-vite)
