DEV Community

Cover image for How I Redesigned a Legacy Website Without Breaking Production
Pixel Mosaic
Pixel Mosaic

Posted on

How I Redesigned a Legacy Website Without Breaking Production

Redesigning a website from scratch sounds exciting.

You get a clean codebase, modern components, better UX, and the opportunity to finally remove all those hacks that have been sitting in production for years.

But there is one problem:

The old website is already working.

Thousands of users might depend on it. Search engines already know its URLs. Analytics are configured. Marketing links are everywhere. APIs expect a particular structure. And somewhere in the codebase, there is probably a !important that nobody remembers adding.

That was the situation I faced when redesigning a legacy website.

The goal wasn't simply to make it look better.

The real goal was:

Replace the experience without breaking the business.

Here's how I approached it.

The Legacy Website Problem

The existing website had accumulated years of changes.

Some parts were modern.

Some parts were old.

And some parts were a combination of both.

The architecture looked roughly like this:

Browser
   |
   v
Legacy UI
   |
   +---- Old components
   |
   +---- New components
   |
   +---- API calls
   |
   +---- Third-party scripts
   |
   +---- Analytics
Enter fullscreen mode Exit fullscreen mode

The biggest mistake would have been treating the project as a completely new application.

The legacy system wasn't just code.

It was also:

  • Existing URLs
  • Existing users
  • Existing SEO rankings
  • Existing analytics
  • Existing API contracts
  • Existing business rules
  • Existing integrations
  • Existing browser behavior

So before writing new UI code, I needed to understand what I was actually replacing.


Step 1: I Mapped the Existing Application

Before touching the UI, I created a simple inventory.

I wanted answers to questions like:

  • Which pages are most important?
  • Which URLs receive the most traffic?
  • Which components are reused?
  • Which API endpoints are critical?
  • Which pages contain business logic?
  • Which third-party scripts are running?
  • What can users actually do on each page?

I created a basic table:

Area Current State Risk Replacement
Homepage Legacy High New
Navigation Legacy High New
Search Mixed High Incremental
Account pages Legacy High Later
Blog Stable Low Keep
Checkout Critical Very High Don't touch initially

This exercise changed the project completely.

I realized I didn't need to rewrite everything.

I needed to identify what actually needed changing.


Step 2: I Defined the "Do Not Break" List

A redesign needs boundaries.

Without them, it's easy to focus entirely on visual improvements while accidentally changing behavior.

I created a list of things that had to continue working.

Functional requirements

  • Login must continue working
  • Search must return the same results
  • Forms must submit correctly
  • Existing APIs must remain compatible
  • Existing navigation paths must work
  • Checkout must remain untouched
  • Existing integrations must continue loading

Technical requirements

  • No breaking API changes
  • No unnecessary database changes
  • No URL changes unless required
  • No large production migration
  • No big-bang deployment

Business requirements

  • No significant SEO loss
  • No analytics data disappearing
  • No conversion-critical flows changing unexpectedly

This became our definition of "safe."


Step 3: I Built the New UI Beside the Old UI

Instead of replacing the entire application, I introduced the new design incrementally.

The architecture became:

                Application
                    |
          +---------+---------+
          |                   |
      Legacy UI           New UI
          |                   |
          +---------+---------+
                    |
                 Shared
                Services
                    |
              APIs / Data
Enter fullscreen mode Exit fullscreen mode

The important part was that both systems could temporarily coexist.

That gave us something incredibly valuable:

a rollback strategy.

If the new implementation failed, we could switch users back to the old one.


Step 4: I Started With the Design System

One of the biggest mistakes in redesign projects is starting with individual pages.

I started with reusable building blocks instead.

For example:

Design System
│
├── Colors
├── Typography
├── Spacing
├── Buttons
├── Inputs
├── Cards
├── Modals
├── Navigation
└── Feedback states
Enter fullscreen mode Exit fullscreen mode

Then pages were composed from those pieces.

Instead of:

Homepage
  ├── custom button
  ├── custom card
  └── custom form

Dashboard
  ├── different button
  ├── different card
  └── different form
Enter fullscreen mode Exit fullscreen mode

we moved toward:

Shared Components
       |
       +---- Homepage
       |
       +---- Dashboard
       |
       +---- Settings
       |
       +---- Search
Enter fullscreen mode Exit fullscreen mode

This made the redesign much easier to scale.

More importantly, it reduced the chance that every page would slowly develop its own version of the new design.


Step 5: I Kept the Data Layer Stable

The temptation during a redesign is to modernize everything at once.

New frontend.

New API.

New database schema.

New authentication.

New deployment system.

That sounds productive.

It is also a fantastic way to create a very difficult debugging session.

Instead, I separated UI changes from data changes.

The new interface could consume the existing APIs.

For example:

async function getUserProfile(userId) {
  const response = await fetch(`/api/users/${userId}`);

  if (!response.ok) {
    throw new Error("Failed to load profile");
  }

  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

The UI could change completely while the underlying contract stayed stable.

That gave us a useful rule:

Change one layer at a time.


Step 6: I Used Feature Flags

One of the most useful techniques during the migration was feature flags.

Instead of:

if (newDesign) {
  renderNewPage();
} else {
  renderOldPage();
}
Enter fullscreen mode Exit fullscreen mode

everywhere in the application, we centralized the decision:

const useNewHomepage = featureFlags.newHomepage;
Enter fullscreen mode Exit fullscreen mode

Then:

return useNewHomepage
  ? <NewHomepage />
  : <LegacyHomepage />;
Enter fullscreen mode Exit fullscreen mode

This gave us control over the rollout.

For example:

Internal users
      ↓
New homepage

5% of users
      ↓
New homepage

25% of users
      ↓
New homepage

50% of users
      ↓
New homepage

100% of users
      ↓
New homepage
Enter fullscreen mode Exit fullscreen mode

If something went wrong, we didn't need to deploy another version.

We could turn the feature off.


Step 7: I Tested the Critical Paths First

A redesign can look perfect and still be broken.

Visual testing wasn't enough.

I identified the flows that mattered most.

For example:

Homepage
   ↓
Search
   ↓
Product
   ↓
Add to cart
   ↓
Checkout
   ↓
Confirmation
Enter fullscreen mode Exit fullscreen mode

And:

Login
   ↓
Dashboard
   ↓
Settings
   ↓
Logout
Enter fullscreen mode Exit fullscreen mode

These became our critical paths.

Before expanding the rollout, we tested them repeatedly.

The key question wasn't:

"Does the new page look correct?"

It was:

"Can a real user still accomplish what they came here to do?"


Step 8: I Paid Attention to URLs

This was one of the easiest things to overlook.

A visual redesign doesn't necessarily require changing URLs.

If the old site had:

/products/123
Enter fullscreen mode Exit fullscreen mode

there was little reason to suddenly change it to:

/catalog/product?id=123
Enter fullscreen mode Exit fullscreen mode

Changing URLs can affect:

  • Search engines
  • Bookmarks
  • External links
  • Analytics
  • Marketing campaigns
  • Browser history
  • Internal references

So whenever possible, I kept existing URLs intact.

When a URL genuinely needed to change, we treated the migration as a separate problem rather than hiding it inside the redesign.


Step 9: I Watched Production Metrics

Once the new UI reached real users, screenshots weren't enough.

We monitored production.

The important metrics depended on the application, but typically included:

  • Error rates
  • JavaScript exceptions
  • API failures
  • Page load performance
  • Conversion rates
  • Search usage
  • Form completion
  • Bounce rates
  • User feedback

A redesign is successful when the experience improves without damaging the metrics that matter.

Sometimes a page can look dramatically better and perform worse.

That's why production data matters.


Step 10: I Released in Small Pieces

The final rollout looked more like this:

Phase 1
  ↓
Design system

Phase 2
  ↓
New navigation

Phase 3
  ↓
Homepage

Phase 4
  ↓
Search

Phase 5
  ↓
Secondary pages

Phase 6
  ↓
Remove legacy components
Enter fullscreen mode Exit fullscreen mode

Not:

6 months of development
        ↓
Friday 5 PM
        ↓
DEPLOY EVERYTHING 🚀
        ↓
production explodes
Enter fullscreen mode Exit fullscreen mode

Small releases made problems easier to isolate.

If something broke after the new navigation was deployed, we knew where to look.

If everything changed simultaneously, debugging would have been much harder.


What I Didn't Rewrite

This might be the most important lesson.

I didn't rewrite everything.

Some parts of the application were old but stable.

And stable code isn't automatically bad code.

If a component:

  • Worked reliably
  • Had good test coverage
  • Didn't block the redesign
  • Didn't create measurable problems

then I left it alone.

Technical debt should be addressed based on its cost, not simply because it is old.

Sometimes the best engineering decision is:

Don't touch it yet.


The Biggest Mistakes I Avoided

1. Big-bang rewrites

Rewriting the entire application creates a huge gap between development and production.

The longer the project takes, the more the two systems drift apart.

Incremental migration keeps that gap smaller.


2. Mixing unrelated refactors

During the redesign, there were plenty of opportunities to say:

"Since we're here, let's also rewrite authentication."

No.

That's how scope explodes.

I tried to keep changes focused.

A redesign should not automatically become an excuse to rewrite every subsystem.


3. Treating visual similarity as success

A page can match the design perfectly and still have:

  • Broken keyboard navigation
  • Slow performance
  • Missing analytics
  • Incorrect API behavior
  • Poor mobile behavior
  • Broken error states

Pixel-perfect isn't the same as production-ready.


4. Removing legacy code too early

New code doesn't become trustworthy the moment it is deployed.

I kept the old implementation available until the new version had proven itself.

Only then did we remove the old code.


The Architecture After the Migration

Eventually, the application moved toward something like:

                 ┌───────────────┐
                 │    Browser    │
                 └───────┬───────┘
                         │
                 ┌───────▼───────┐
                 │   New UI      │
                 └───────┬───────┘
                         │
              ┌──────────▼──────────┐
              │ Shared Components   │
              │ + Services          │
              └──────────┬──────────┘
                         │
              ┌──────────▼──────────┐
              │ Existing APIs       │
              └──────────┬──────────┘
                         │
                 ┌───────▼───────┐
                 │ Existing Data │
                 └───────────────┘
Enter fullscreen mode Exit fullscreen mode

The important thing is that the migration happened around the existing production system, not by destroying it first.


What I Learned

The biggest lesson wasn't about React, CSS, JavaScript, or any particular framework.

It was about risk management.

A legacy redesign is fundamentally a migration problem.

You are moving users from one system to another while the business continues operating.

That means the best strategy is usually:

  1. Understand what exists.
  2. Identify what cannot break.
  3. Separate UI changes from backend changes.
  4. Build reusable components.
  5. Introduce the new experience incrementally.
  6. Use feature flags.
  7. Monitor real production behavior.
  8. Roll out gradually.
  9. Keep rollback possible.
  10. Remove the old implementation only after the new one is proven.

The goal isn't to make the old code disappear as quickly as possible.

The goal is to make the new system trustworthy enough that you no longer need the old one.


Final Takeaway

If you're about to redesign a legacy website, don't ask:

"How can I rebuild this from scratch?"

Ask:

"How can I move users from the old experience to the new one without creating unnecessary risk?"

That small change in perspective can completely change your architecture, deployment strategy, and development process.

A successful redesign isn't the one with the most rewritten code.

It's the one where users get a better experience and nobody gets paged at 2 AM because production went down.

And honestly, that's the kind of redesign I'm happy to ship.

Top comments (0)