smallbox

← All work

Case study

MVC monolith → REST API + modern frontend

The tension

A long-running ASP.NET MVC application has worked for years. Razor views, controllers that mix data access and presentation, business rules scattered across action methods and partial views. The system works — but every change requires touching too many places, and the team can no longer hire people who want to write Razor.

Why the naive solution fails

Most rewrites go like this: announce the migration, start a parallel project, freeze the old system, ship in eighteen months. By month twelve, the new system is missing a hundred small features the team forgot existed. The old system has accumulated bugs the team can no longer fix because everyone is working on the new one. Either the rewrite is abandoned, or cutover happens with a long bug tail, or the team gives up and runs both forever.

The shared failure mode is treating migration as a single moment.

The design rule

The old system runs until the new one is ready. Each page is migrated end-to-end, reversibly, by a routing flip. There is never a moment where the system is broken. The team is never gated on the rewrite for new features.

What was actually built

Three phases, repeatable per page.

Phase 1 — extract the API from inside the MVC app. The MVC controllers stay running. Alongside them, REST API controllers are added — using the same business services, the same data access, the same database. The new API is a parallel surface, not a replacement.

┌──────────────────────────────────────────────────┐
│ Existing MVC application (still serving traffic) │
├──────────────────────────────────────────────────┤
│ Razor Controllers   │   REST API Controllers     │
│ (existing pages)    │   (new — added one         │
│                     │    at a time)              │
└──────────────┬──────────────────────────┬────────┘
               │                          │
               └──────────┬───────────────┘
                          │
                  Shared business services,
                  repositories, database

Phase 2 — build the new frontend, page by page. A Next.js app starts consuming the REST API. The first page is migrated end-to-end:

  1. REST endpoints exist for the page's data needs.
  2. The Next.js page is built and tested.
  3. nginx routes that one URL to Next.js, leaving every other URL on MVC.
  4. The MVC view is removed once the Next.js page is verified.

Then the second page. Then the third. Each migration is small, reversible, and doesn't depend on the others.

Phase 3 — cut over the deployment. When all user-facing pages are on Next.js, the MVC app shrinks to admin or internal pages, or is retired entirely. The cut is a routing change, not a launch.

Where the work actually is

The visible work is the new Next.js pages. The real work is extracting the API cleanly:

  • DTOs become explicit. What the MVC view assembled inline, the API now returns as structured data. Implicit ViewModels become formal contracts.
  • Business rules surface. Razor views often contain logic — @if (Model.User.HasActiveSubscription). That logic moves into BusinessServices or DTO assembly.
  • Authentication transitions. Cookie-based forms auth → JWT, often the trickiest part.
  • Cross-cutting concerns relocate. Anti-forgery, CSRF, session, error pages, redirects — all need a deliberate home in the new frontend.
  • Tests can be added. API endpoints are testable in a way Razor views were not. The migration is a chance to introduce real integration testing — see DB-per-test through real layers.

A page migration that looks like "two days of frontend work" is often two weeks: one for the API and tests, a few days for the frontend, the rest for the cross-cutting cleanup.

Where this pattern came from

Drawn from ServiceByen.dk — a Danish service-marketplace platform built on ASP.NET MVC and SQL Server, operated solo for 13 years. Booking flows, messaging, payment logic, search, admin tools, customer support — all running through Razor controllers and views accumulated over a decade. The migration discipline above is what made it possible to evolve that codebase without ever taking it offline.

Honest framing: this pattern is not yet delivered as an external Smallbox engagement. The discipline comes from operating ServiceByen.dk through every phase. A Smallbox engagement applying this pattern to a client codebase would be a first; the System Report would scope the migration explicitly before any code is moved.

What this proves Smallbox can do

Move legacy systems forward without freezing them. Real users on the new stack from day one. Reversible at every step. No big-bang launch, no eighteen-month deadline, no system that's broken in the middle.

Want your product in this shape?

← All work