# Vite To Nextjs App Router

> 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.

- Skill: `lucaszhh/vite-to-nextjs-app-router` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lucaszhh/vite-to-nextjs-app-router`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lucaszhh/vite-to-nextjs-app-router/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: lucaszhh (https://skillmd.com/u/lucaszhh)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lucaszhh/vite-to-nextjs-app-router

---


# 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

1. **Never a big-bang rewrite.** Migrate route-by-route behind a feature boundary. The old app keeps serving until a route is verified.
2. **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.
3. **Auth/session bridges, doesn't fork.** Keycloak (or whatever IdP) session is shared; do not build a parallel auth flow in Next.
4. **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)

- **Diff de respuesta / HTML**: Validar que la misma URL devuelva la misma información comparando el payload localmente:
  ```bash
  # Diff de HTML o response entre Vite (puerto 5173) y Next.js (puerto 3000):
  diff -u <(curl -s http://localhost:5173/ruta) <(curl -s http://localhost:3000/ruta)
  ```
- Same auth state is respected (logged-in user stays logged-in).
- No regression in client interactivity for mutation-heavy routes.

## 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.


