Skip to content
Back to Maintenance & LTS

Framework Migration

Replace one framework with another (e.g. Express→Fastify, Vue 2→3) incrementally, not in a big bang.

Why

A framework migration touches every route, every middleware, every test — it's the highest-blast-radius change in software. A big-bang migration means a multi-week branch where nothing else ships, and when it breaks (it will), you can't tell which of 200 changes caused it. The incremental approach — the strangler fig pattern — routes one endpoint at a time to the new framework, with the old framework as a fallback. Each migrated endpoint is its own PR, its own deploy, its own rollback. You ship features throughout the migration, and you can stop at any point if priorities change. This is how you migrate a 500-route app without a freeze.

When

When the current framework is EOL or blocking a needed feature. Never migrate for novelty — the cost is always higher than estimated.

How

1. Put a router/proxy in front (nginx, or an in-process router) that can route per-path to old or new
2. Pick the lowest-risk endpoint first (a read-only, low-traffic route) to validate the approach
3. Implement that one endpoint in the new framework; route just that path to the new app
4. Run both in production: shadow traffic to the new one, compare responses, alert on diff
5. Cut over the one endpoint to the new framework; keep the old code for instant rollback
6. Migrate the next endpoint; repeat. Each is its own PR + deploy + monitoring window
7. Once the last endpoint is migrated, remove the old framework entirely
8. Throughout, ship features in the not-yet-migrated endpoints — the migration must not freeze the product

Example

# Strangler fig: nginx routes per-path to old (Express) or new (Fastify)
location /api/health { proxy_pass http://fastify:3001; }  # migrated
location /api/      { proxy_pass http://express:3000; }   # not yet

# Shadow traffic: log diff between old and new for the same request
app.get('/api/orders', async (req, res) => {
  const [oldRes, newRes] = await Promise.all([
    expressHandler(req), fastifyHandler(req)
  ]);
  if (JSON.stringify(oldRes) !== JSON.stringify(newRes)) {
    logger.error({ oldRes, newRes, path: req.path }, 'migration diff');
  }
  res.json(oldRes);  // still serve old until cut-over
});

# Cut over one endpoint (its own PR + deploy)
# nginx: change /api/orders -> fastify:3001
# Monitor error rate for 1h; if degraded, revert nginx (instant rollback)

# Each migrated endpoint is a PR:
# "migrate: /api/health to fastify"
# "migrate: /api/orders to fastify"
# ... 500 PRs later, remove Express entirely.

Common Problems

  • ⚠A big-bang migration branch — 6 weeks of no features, and when it breaks you can't bisect 200 intermingled changes.
  • ⚠No shadow traffic — you cut over an endpoint and discover at 3am that the new framework serializes dates differently.
  • ⚠Freezing feature work during the migration — the business loses patience and cancels the migration halfway, leaving you on two frameworks forever.

Best Practices

  • ✓Use the strangler fig pattern: migrate one endpoint at a time with a proxy routing per-path — each is independently deployable and revertible.
  • ✓Shadow-traffic every endpoint before cutover: send the same request to old and new, alert on any response diff.
  • ✓Never freeze feature work during a migration — ship features in unmigrated endpoints; the migration is background, not a stop-the-world event.