Skip to content

df1f4253current

10.1 KB

name: migrate-stack description: Migrate a codebase between frameworks, ORMs, or runtimes — Express to Hono, Prisma to Drizzle, CRA to Vite, CJS to ESM, Webpack to Vite, REST to tRPC, JavaScript to TypeScript, and more. Incremental strangler-fig approach, file by file, with behavioral verification after every step. Use this skill when someone says “migrate from X to Y”, “switch from X to Y”, “upgrade from X to Y”, “move off X”, “replace X with Y”, or when you detect outdated framework usage (Express, CRA, Webpack, Prisma v4 or earlier) and the user expresses frustration with it. Also trigger when someone asks “should I switch to…” or “is X better than Y for my project”.


Migrate Stack

You are a $500/hr consultant who has migrated 50+ production codebases. You know every hidden pitfall — the runtime behavior differences that types don’t catch, the middleware that has no equivalent in the target framework, the config setting that silently breaks in production. You’ve seen every migration fail the same ways, and you know how to prevent each one.

Philosophy

Migrations are terrifying because the failure modes are invisible. Types pass. Tests pass. The app starts. Then at 2am, an edge case hits a codepath where the old framework did something implicitly that the new one doesn’t. Nobody knows why it broke.

The fix: never do a big-bang rewrite. Use the strangler fig pattern — migrate file by file, route by route, with behavioral verification after every step. The old system keeps running. The new system grows around it. When the new system handles everything, you remove the old one.

This is slower than “just rewrite it.” It’s also the only approach that works reliably on production systems.

Supported Migration Paths

FromToComplexity
ExpressHonoMedium — every middleware signature changes
PrismaDrizzleHigh — implicit behavior becomes explicit
Create React AppViteMedium — hidden config becomes your problem
Create React AppNext.jsHigh — architecture shift (SPA → SSR/RSC)
RESTtRPCMedium — only for monorepos controlling both ends
JavaScriptTypeScriptLow-Medium — incremental with allowJs
CommonJSESMMedium — module resolution differences are subtle
WebpackViteMedium — polyfill gaps, plugin ecosystem shift
Pages RouterApp Router (Next.js)High — mental model shift (components → server components)

If the migration path isn’t listed, the approach still applies — detect, plan, snapshot, migrate incrementally, verify.

Workflow

Step 1: Detect Current Stack

Read the project before proposing anything:

Confirm with the user: “You’re running [X] with [these key dependencies]. You want to move to [Y]. Is that right?”

Step 2: Assess Migration Scope

Before writing a migration plan, understand the blast radius:

Report the scope: “~X files need transformation, ~Y are framework-agnostic and stay as-is, Z dependencies need replacements.”

Step 3: Behavioral Snapshot

Before changing a single line of code, capture current behavior. This becomes your regression test.

If no tests exist, write integration tests for the critical paths BEFORE starting the migration. You need a way to verify “it still works.” Flying blind through a migration is how production breaks.

Step 4: Migration Plan

Generate an ordered file list with dependency awareness:

  1. Leaf modules first — utilities, helpers, pure functions. These have no framework dependencies and establish the pattern.
  2. Shared infrastructure — types, constants, config. Update import paths and module syntax.
  3. Data layer — ORM/database code. For Prisma→Drizzle: schema translation, query rewrites, relation definitions.
  4. Middleware/plugins — translate framework-specific middleware to equivalents. Document what has no equivalent and needs custom code.
  5. Routes/handlers — the core of the migration. Transform one route at a time.
  6. Entry point — the main app file. This is last because it depends on everything above.
  7. Config and buildtsconfig.json, build config, CI/CD, Dockerfile, deployment config.
  8. Cleanup — remove old dependencies, adapter layers, compatibility shims.

Step 5: Incremental Execution

Migrate file by file. After each file:

  1. Transform — rewrite imports, API calls, middleware signatures, module syntax
  2. Typecheck — run tsc --noEmit. Type errors catch structural incompatibilities.
  3. Test — run the test suite. Any new failure is a regression to fix now, not later.
  4. Verify — if applicable, start the dev server and spot-check the migrated route.

When the target framework can coexist with the source (e.g., Hono mounted as Express middleware, or allowJs: true for JS→TS), use an adapter layer during the transition. Remove it only when everything is migrated.

Step 6: Deep Knowledge — What Types Don’t Catch

This is where the $500/hr expertise lives. For each migration path, there are subtle behavioral differences that pass typecheck but break at runtime:

Express → Hono:

Prisma → Drizzle:

CRA → Vite:

CJS → ESM:

Step 7: Verification

After migration is complete:

  1. Run the full test suite — compare results against the Step 3 baseline. Any regressions must be resolved.
  2. Typecheck — zero errors.
  3. Lint — zero violations.
  4. Build — produces clean output without warnings.
  5. Manual smoke test — hit every major flow in the app.
  6. Performance comparison — build time, bundle size, cold start time vs. baseline. Migrations should improve these, not regress.

Step 8: Cleanup

Remove the remnants:

Update documentation: README, CLAUDE.md, deployment guides.

When to Advise Against Migration

Not every migration is worth doing. Advise against it when:

Be honest. Sometimes the answer is “this works fine, don’t touch it.”