Vite to Next.js (App Router) Migration
A migration skill, not a vibe. Every step below is a decision I made (and defended) on a production migration serving +500k users, so the agent reproduces a route that already shipped rather than improvising.
When to use
- "Migrate this Vite app to Next.js"
- "Switch to App Router / file-based routing"
- "Add SSR / server components to my React SPA"
- "Move off react-router onto Next.js routing"
If the app is a marketing site with no auth and no client state, this is overkill. This skill earns its keep on authenticated, stateful SPAs with real routes and API consumption.
Iron rules
- Never a big-bang rewrite. Migrate route-by-route behind a feature boundary. The old app keeps serving until a route is verified.
- TanStack Query stays. Do not rip it out for "pure" server components on day one. Keep the cache for client-side mutations; add Server Components only where the payoff (SEO, initial paint, reduced client JS) is concrete.
- Auth/session bridges, doesn't fork. Keycloak (or whatever IdP) session is shared; do not build a parallel auth flow in Next.
- Validation lives in Zod, shared. One schema source for client + server + form. Do not hand-roll validators per layer.
Migration order (do it in this sequence)
0. Audit before touching code
- Map every route in
react-router (including nested + dynamic params + redirects).
- List every
useEffect data fetch. These are Server Component candidates.
- List every mutation / optimistic UI. These stay client.
- Identify the auth boundary: where is the session read? (Keycloak token, cookie, context?)
1. Scaffold Next.js alongside, not over
npx create-next-app@latest with App Router, TypeScript, the same styling lib (Material UI / Tailwind / Shadcn).
- Do not delete the Vite app yet. Run Next on a different port; reverse-proxy or feature-flag route-by-route.
2. Routing first, components second
- Translate
react-router paths to App Router file conventions:
/users/:id becomes app/users/[id]/page.tsx
- layout routes become
layout.tsx
- index becomes
page.tsx
* becomes not-found.tsx
- Keep the exact same URL surface. Renaming URLs is a separate, deliberate task, not a side effect of migration.
3. Auth/session bridge
- Read the existing session mechanism (Keycloak:
keycloak-js on the client, or a server-side session cookie).
- For Keycloak: keep the client adapter for the transition; expose the token via a provider so migrated routes can read it. Move to server-side session verification on protected routes once the route is cut over.
- Do not invent a new auth flow. If the old app reads
window.keycloak, the new route reads the same source until cutover.
4. Data fetching: Server Components where it pays
- For each
useEffect fetch from step 0, ask: does this need client interactivity after load?
- No -> convert to an
async Server Component. Pass typed data to a client child for interactivity.
- Yes (mutations, optimistic) -> keep
useQuery in a "use client" component.
- Keep TanStack Query for all client-side cache + mutations. The migration is not the moment to also change your data library.
5. Forms and validation
react-hook-form + zod move as-is into "use client" components.
- Define Zod schemas in a shared
lib/schemas/ so Server Components and Client Components import the same source.
- Server Actions become the eventual target for submits, but only after the route is stable with the existing API. Don't couple the migration to an API rewrite.
6. Styling
- Material UI / Tailwind / Shadcn: carry over directly. Material UI's SSR works in App Router; register the
AppRouterCacheProvider in the root layout.
- Modular CSS files: import paths change, semantics don't.
7. Cut over one route at a time
- For each route: build the Next version, verify against the Vite version (same data, same URL, same auth), then flip the proxy/flag.
- Keep the Vite route reachable for one release as a rollback. Only delete it once the Next route has served real traffic without incident.
8. Cleanup
- Remove the Vite app,
react-router, and the old proxy once every route is cutover.
- Delete dead
useEffect fetches that became Server Components.
- Update CI to build only Next.
Anti-patterns to refuse
- "Let's also redesign the API while we migrate." -> No. One change at a time.
- "Replace TanStack Query with RSC fetch everywhere." -> No. Mutations and optimistic UI stay client.
- "Rename the URLs to be cleaner." -> No. Separate task, separate PR.
- "Big-bang: delete Vite, ship Next in one release." -> No. Route-by-route, with rollback.
Matriz de Progreso y Corte de Rutas (docs/migration-matrix.md)
Mantener el seguimiento exhaustivo de la migración utilizando la matriz de corte ruta por ruta:
| Ruta de la App |
Estado (Vite / Next) |
Auth Verificada |
Tipo (RSC / Client) |
Rollback Listo |
Notas / Dependencias |
/login |
Next.js |
✅ Keycloak SSO |
Client ("use client") |
✅ Vite port 5173 |
Redirección con callback URL |
/dashboard |
Next.js |
✅ Session cookie |
Server Component |
✅ Proxy fallback |
Fetch inicial SSR de trámites |
/procedures/:id |
Vite |
⏳ Pendiente |
Client (TanStack Query) |
N/A |
Mutaciones optimistas complejas |
Verification (what "done" looks like per route)
Provenance
Codified from the Mendoza por Mí migration (Necta, 2025): Vite + React Router SPA serving +500k users migrated to Next.js App Router, route-by-route, with Keycloak auth and TanStack Query retained for mutations. The decisions above are the ones that shipped, not a generic checklist.
1---2name: vite-to-nextjs-app-router3description: Migrate a Vite + React Router SPA to Next.js App Router in production. Codified from a real migration on a +500k-user government platform (Mendoza por Mí). Use when the user asks to migrate a Vite/CRA app to Next.js, or to adopt App Router routing, SSR, or server components incrementally.4---56# Vite to Next.js (App Router) Migration78A migration skill, not a vibe. Every step below is a decision I made (and defended) on a production migration serving +500k users, so the agent reproduces a route that already shipped rather than improvising.910## When to use1112- "Migrate this Vite app to Next.js"13- "Switch to App Router / file-based routing"14- "Add SSR / server components to my React SPA"15- "Move off react-router onto Next.js routing"1617If the app is a marketing site with no auth and no client state, this is overkill. This skill earns its keep on **authenticated, stateful SPAs with real routes and API consumption**.1819## Iron rules20211. **Never a big-bang rewrite.** Migrate route-by-route behind a feature boundary. The old app keeps serving until a route is verified.222. **TanStack Query stays.** Do not rip it out for "pure" server components on day one. Keep the cache for client-side mutations; add Server Components only where the payoff (SEO, initial paint, reduced client JS) is concrete.233. **Auth/session bridges, doesn't fork.** Keycloak (or whatever IdP) session is shared; do not build a parallel auth flow in Next.244. **Validation lives in Zod, shared.** One schema source for client + server + form. Do not hand-roll validators per layer.2526## Migration order (do it in this sequence)2728### 0. Audit before touching code29- Map every route in `react-router` (including nested + dynamic params + redirects).30- List every `useEffect` data fetch. These are Server Component candidates.31- List every mutation / optimistic UI. These **stay client**.32- Identify the auth boundary: where is the session read? (Keycloak token, cookie, context?)3334### 1. Scaffold Next.js alongside, not over35- `npx create-next-app@latest` with **App Router**, **TypeScript**, the same styling lib (Material UI / Tailwind / Shadcn).36- Do **not** delete the Vite app yet. Run Next on a different port; reverse-proxy or feature-flag route-by-route.3738### 2. Routing first, components second39- Translate `react-router` paths to App Router file conventions:40 - `/users/:id` becomes `app/users/[id]/page.tsx`41 - layout routes become `layout.tsx`42 - index becomes `page.tsx`43 - `*` becomes `not-found.tsx`44- Keep the **exact** same URL surface. Renaming URLs is a separate, deliberate task, not a side effect of migration.4546### 3. Auth/session bridge47- Read the existing session mechanism (Keycloak: `keycloak-js` on the client, or a server-side session cookie).48- For Keycloak: keep the client adapter for the transition; expose the token via a provider so migrated routes can read it. Move to server-side session verification on protected routes once the route is cut over.49- Do **not** invent a new auth flow. If the old app reads `window.keycloak`, the new route reads the same source until cutover.5051### 4. Data fetching: Server Components where it pays52- For each `useEffect` fetch from step 0, ask: does this need client interactivity after load?53 - **No** -> convert to an `async` Server Component. Pass typed data to a client child for interactivity.54 - **Yes (mutations, optimistic)** -> keep `useQuery` in a `"use client"` component.55- Keep **TanStack Query** for all client-side cache + mutations. The migration is not the moment to also change your data library.5657### 5. Forms and validation58- `react-hook-form` + `zod` move as-is into `"use client"` components.59- Define Zod schemas in a shared `lib/schemas/` so Server Components and Client Components import the same source.60- Server Actions become the eventual target for submits, but only after the route is stable with the existing API. Don't couple the migration to an API rewrite.6162### 6. Styling63- Material UI / Tailwind / Shadcn: carry over directly. Material UI's SSR works in App Router; register the `AppRouterCacheProvider` in the root layout.64- Modular CSS files: import paths change, semantics don't.6566### 7. Cut over one route at a time67- For each route: build the Next version, verify against the Vite version (same data, same URL, same auth), then flip the proxy/flag.68- Keep the Vite route reachable for one release as a rollback. Only delete it once the Next route has served real traffic without incident.6970### 8. Cleanup71- Remove the Vite app, `react-router`, and the old proxy once every route is cutover.72- Delete dead `useEffect` fetches that became Server Components.73- Update CI to build only Next.7475## Anti-patterns to refuse7677- "Let's also redesign the API while we migrate." -> No. One change at a time.78- "Replace TanStack Query with RSC fetch everywhere." -> No. Mutations and optimistic UI stay client.79- "Rename the URLs to be cleaner." -> No. Separate task, separate PR.80- "Big-bang: delete Vite, ship Next in one release." -> No. Route-by-route, with rollback.8182## Matriz de Progreso y Corte de Rutas (`docs/migration-matrix.md`)8384Mantener el seguimiento exhaustivo de la migración utilizando la matriz de corte ruta por ruta:8586| Ruta de la App | Estado (Vite / Next) | Auth Verificada | Tipo (RSC / Client) | Rollback Listo | Notas / Dependencias |87|---|:---:|:---:|:---:|:---:|---|88| `/login` | Next.js | ✅ Keycloak SSO | Client (`"use client"`) | ✅ Vite port 5173 | Redirección con callback URL |89| `/dashboard` | Next.js | ✅ Session cookie | Server Component | ✅ Proxy fallback | Fetch inicial SSR de trámites |90| `/procedures/:id` | Vite | ⏳ Pendiente | Client (TanStack Query) | N/A | Mutaciones optimistas complejas |9192## Verification (what "done" looks like per route)9394- **Diff de respuesta / HTML**: Validar que la misma URL devuelva la misma información comparando el payload localmente:95 ```bash96 # Diff de HTML o response entre Vite (puerto 5173) y Next.js (puerto 3000):97 diff -u <(curl -s http://localhost:5173/ruta) <(curl -s http://localhost:3000/ruta)98 ```99- Same auth state is respected (logged-in user stays logged-in).100- No regression in client interactivity for mutation-heavy routes.101102## Provenance103104Codified from the Mendoza por Mí migration (Necta, 2025): Vite + React Router SPA serving +500k users migrated to Next.js App Router, route-by-route, with Keycloak auth and TanStack Query retained for mutations. The decisions above are the ones that shipped, not a generic checklist.105