---
title: "Incremental Migration"
description: "Apply the strangler fig pattern to migrate from legacy applications to microfrontends piece by piece."
canonical_url: "https://vercel.com/academy/microfrontends-on-vercel/incremental-migration"
md_url: "https://vercel.com/academy/microfrontends-on-vercel/incremental-migration.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T19:45:13.343Z"
content_type: "lesson"
course: "microfrontends-on-vercel"
course_title: "Microfrontends on Vercel"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Incremental Migration

# Incremental Migration

Not everyone starts with a greenfield project. Many teams have legacy applications on another CDN or using an older framework that need to be modernized. Microfrontends enable migration without big-bang rewrites.

## Outcome

Understand the strangler fig pattern and how to migrate routes from a legacy application to microfrontends incrementally.

## The Strangler Fig Pattern

Named after fig trees that gradually envelop and replace their host trees, this pattern:

1. **Wrap the legacy system** - Put Vercel in front of everything
2. **Carve out routes** - Move routes one at a time to new microfrontends
3. **Strangle the legacy** - Eventually, no routes go to the old system
4. **Remove the host** - Decommission the legacy application

```
Before:
legacy.example.com → [Legacy App] → All routes

During:
example.com → [Vercel] → /           → [New Marketing]
                       → /docs       → [New Docs]
                       → /*          → [Legacy App]

After:
example.com → [Vercel] → /           → [Marketing]
                       → /docs       → [Docs]
                       → /app        → [Dashboard]
                       → (legacy decommissioned)
```

## The Proxy Pattern

When your legacy app isn't on Vercel, create a proxy project:

```typescript title="apps/legacy-proxy/middleware.ts"
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // Proxy all requests to the legacy application
  const legacyUrl = new URL(
    request.nextUrl.pathname + request.nextUrl.search,
    "https://legacy.example.com"
  );

  return NextResponse.rewrite(legacyUrl);
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```

This project acts as a bridge: it's a Vercel project that forwards requests to your legacy system.

## Fast Track

1. Create a proxy project that forwards to an external legacy URL
2. Configure it as the default microfrontend
3. Carve out one route to a new microfrontend

## Migration Strategy

### Phase 1: Proxy Everything

```json
{
  "applications": {
    "@acme/legacy-proxy": {
      "development": {
        "fallback": "http://localhost:3000",
        "local": 3000
      }
    }
  }
}
```

All traffic flows through Vercel to the legacy system. Nothing changes for users.

### Phase 2: Carve Out First Route

```json
{
  "applications": {
    "@acme/legacy-proxy": {
      "development": {
        "fallback": "http://localhost:3000",
        "local": 3000
      }
    },
    "@acme/docs": {
      "routing": [
        { "paths": ["/docs", "/docs/:path*"] }
      ],
      "development": {
        "local": 3001
      }
    }
  }
}
```

`/docs` now serves from the new docs microfrontend. Everything else still goes to legacy.

### Phase 3: Feature Flag the Migration

```typescript
// In legacy-proxy middleware
const pathname = request.nextUrl.pathname;

if (pathname.startsWith("/app")) {
  const useNewDashboard = await checkFeatureFlag("new-dashboard");
  if (useNewDashboard) {
    // Rewrite to new dashboard microfrontend
    return NextResponse.rewrite(new URL("/app-new" + pathname.slice(4), request.url));
  }
}

// Fall through to legacy
return NextResponse.rewrite(new URL(pathname, "https://legacy.example.com"));
```

### Phase 4: Increase Rollout

```
Week 1: /docs → new docs app (feature flagged, 10%)
Week 2: /docs → new docs app (50%)
Week 3: /docs → new docs app (100%, remove flag)
Week 4: /app → new dashboard (feature flagged, 10%)
...
```

### Phase 5: Remove Legacy

Once all routes are migrated:

```json
{
  "applications": {
    "@acme/marketing": {
      "development": {
        "fallback": "http://localhost:3000",
        "local": 3000
      }
    },
    "@acme/docs": {
      "routing": [
        { "paths": ["/docs", "/docs/:path*"] }
      ],
      "development": {
        "local": 3001
      }
    },
    "@acme/dashboard": {
      "routing": [
        { "paths": ["/app", "/app/:path*"] }
      ],
      "development": {
        "local": 3002
      }
    }
  }
}
```

The legacy proxy project is no longer needed.

## Hands-on Exercise 4.2

Create a legacy proxy that forwards to an external URL, simulating the first step of an incremental migration.

### Part 1: Create the Legacy Proxy App

Create a new app that will proxy requests to httpbin (a test service):

```bash
cd apps
pnpm create next-app@latest legacy-proxy --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*"
cd legacy-proxy
pnpm add @vercel/microfrontends
```

### Part 2: Configure the Proxy Middleware

Create `apps/legacy-proxy/middleware.ts`:

```typescript title="apps/legacy-proxy/middleware.ts"
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;

  // Proxy /legacy/* paths to httpbin for demonstration
  if (pathname.startsWith("/legacy")) {
    const targetPath = pathname.replace("/legacy", "");
    const legacyUrl = new URL(
      targetPath || "/get",
      "https://httpbin.org"
    );

    console.log(`[proxy] ${pathname} → ${legacyUrl.toString()}`);
    return NextResponse.rewrite(legacyUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/legacy/:path*"],
};
```

### Part 3: Update Microfrontends Configuration

Update `apps/marketing/microfrontends.json` to include the proxy:

```json title="apps/marketing/microfrontends.json"
{
  "$schema": "https://openapi.vercel.sh/microfrontends.json",
  "applications": {
    "@acme/marketing": {
      "development": {
        "fallback": "http://localhost:3000",
        "local": 3000
      }
    },
    "@acme/docs": {
      "routing": [
        { "paths": ["/docs", "/docs/:path*"] }
      ],
      "development": {
        "local": 3001
      }
    },
    "@acme/dashboard": {
      "routing": [
        { "paths": ["/app", "/app/:path*", "/settings", "/settings/:path*"] }
      ],
      "development": {
        "local": 3002
      }
    },
    "@acme/legacy-proxy": {
      "routing": [
        { "paths": ["/legacy", "/legacy/:path*"] }
      ],
      "development": {
        "local": 3004
      }
    }
  },
  "options": {
    "localProxyPort": 3024
  }
}
```

### Part 4: Test the Proxy

Run the dev server:

```bash
pnpm dev
```

Test the proxy routes:

1. Visit `http://localhost:3024/legacy/get` - Should return JSON from httpbin
2. Visit `http://localhost:3024/legacy/headers` - Shows request headers
3. Visit `http://localhost:3024/` - Still serves marketing (not proxied)

## Try It

Verify the migration pattern works:

```bash
# Should proxy to httpbin
curl http://localhost:3024/legacy/get

# Should return JSON like:
{
  "args": {},
  "headers": { ... },
  "origin": "...",
  "url": "https://httpbin.org/get"
}
```

## Commit

```bash
git add -A
git commit -m "feat: add legacy proxy for incremental migration demo"
```

## Real-World Example: Notion

From the webinar, Notion's migration approach:

1. **Legacy:** Old Pages Router application serving everything
2. **Strategy:** Carve out sections into new App Router apps
3. **Implementation:** Feature flags for gradual rollout per route
4. **Monitoring:** Watch for errors before removing legacy code
5. **Cleanup:** Remove legacy routes once stable

## Migration Checklist

| Phase           | Action                     | Validation                   |
| --------------- | -------------------------- | ---------------------------- |
| Setup           | Create proxy project       | Legacy accessible via Vercel |
| First route     | Carve out low-risk route   | Route serves from new app    |
| Feature flag    | Enable for internal users  | Both paths work              |
| Gradual rollout | Increase percentage        | Monitor error rates          |
| Full migration  | Remove flag, update config | 100% on new app              |
| Cleanup         | Remove proxy, decommission | Legacy shut down             |

## Monitoring During Migration

Key metrics to watch:

- **Error rates** - Compare legacy vs new for same routes
- **Response times** - New app should be equal or faster
- **User feedback** - Support tickets, bug reports
- **Conversion rates** - Business metrics shouldn't regress

If any metric regresses significantly, roll back by disabling the feature flag.

## Done-When

- [ ] You understand the strangler fig pattern
- [ ] You know how to create a proxy to a legacy application
- [ ] You can plan a phased migration with feature flags
- [ ] You know what metrics to monitor during migration

## When to Use This Pattern

| Scenario                            | Good Fit?                |
| ----------------------------------- | ------------------------ |
| Legacy app on another CDN           | Yes                      |
| Monolith you want to split          | Yes                      |
| Framework migration (CRA → Next.js) | Yes                      |
| Small app rewrite                   | Probably overkill        |
| Tightly coupled legacy system       | Challenging but possible |

## The Risk-Reduction Benefit

Traditional rewrite:

```
Months of work → Big bang launch → Hope nothing breaks
```

Incremental migration:

```
Route 1 migrated → Test → Route 2 migrated → Test → ...
Each step is reversible. Risk is contained.
```

## What's Next

Horizontal microfrontends: when different applications contribute components to the same page.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
