Migrating a Real Next.js App

Lesson 8 — apply everything on a codebase you own (wisdom track)

Lessons 1–7 taught you the primitives in a sandbox. Wisdom — real retention — only comes from applying them on a messy, real codebase. This lesson is a guided migration playbook: take a Next.js app you own, convert it route-by-route to TanStack Router, and discover the edge cases no lesson can predict.

Why This Is the Wisdom Track

Knowledge and skills you've built in isolation feel solid. Real apps break that illusion:

You will hit surprises. That's the point. Each one is a learning record waiting to be written.

The Migration Plan

Six phases. Do not skip ahead — each builds on the previous. Expect each phase to surface questions worth bringing back to your teacher.

Step 1 — Install & create the router

Install TanStack Router + the Vite plugin (or your bundler's equivalent). The plugin watches your routes/ folder and generates routeTree.gen.ts — the typed route tree that powers all the type safety from Lessons 5–7.

npm install @tanstack/react-router
npm install -D @tanstack/router-plugin vite

Wire the plugin in vite.config.ts, then create your router in a router.ts file pointing at the generated route tree. You can run Next.js and TanStack side-by-side during migration — they don't have to swap in one shot.

Step 2 — Map your route tree on paper first

Before touching files, draw the route tree. For each Next.js route:

  1. Does it own a URL segment? If yes → directory or $param.
  2. Is it structural chrome? If yes → pathless layout (_name).
  3. Is it purely organizational? If yes → group ((name)).

Translate Next.js conventions to TanStack (recap from Lessons 1–4):

Next.jsTanStack
app/.../page.tsxroutes/.../index.tsx
app/.../layout.tsxroutes/.../route.tsx with <Outlet/>
app/[slug]/page.tsxroutes/$slug/index.tsx
app/(group)/page.tsxroutes/(group)/index.tsx
app/[...slug]/page.tsxroutes/$/index.tsx (splat, _splat)

Step 3 — Convert one route, end-to-end

Pick the smallest leaf route in your app — a static page like /about. Convert it fully:

  1. Create routes/about/index.tsx
  2. createFileRoute('/about')({ component: AboutPage })
  3. Move the JSX from page.tsx into the component
  4. Verify it renders at /about via the TanStack router

One green route proves the pipeline. Resist converting everything at once.

Step 4 — Migrate data fetching route-by-route

For each route with data (Lesson 7):

For params-dependent routes, declare loader: ({ params, signal }) => .... For search-dependent routes, add validateSearch + loaderDeps.

Step 5 — Rewire navigation (Lesson 5)

Find every <Link href="..."> and router.push(...). Replace:

// Before (Next.js)
<Link href={`/posts/${post.id}`}>{post.title}</Link>
router.push(`/posts/${newId}`)

// After (TanStack)
<Link to="/posts/$postId" params={{ postId: post.id }}>{post.title}</Link>
navigate({ to: '/posts/$postId', params: { postId: newId } })

The conversion is mechanical but catches bugs: any broken string interpolation surfaces as a TypeScript error now.

Step 6 — Promote UI state to search params (Lesson 6)

For each useState that holds shareable UI state — filters, pagination, modal-open, selected tab — ask: should this be in the URL? If yes:

  1. Add a Zod validateSearch schema with .catch() fallbacks
  2. Read via Route.useSearch()
  3. Write via navigate({ search: prev => ({ ...prev, ... }) })
  4. Delete the useState

This is the highest-leverage refactor — shareable URLs and natural back-button behavior for free.

Gotchas to Expect

⚠️ Server components vs client loaders Next.js App Router runs loaders on the server by default. TanStack Router (without TanStack Start) runs them on the client. Data that was server-only (DB secrets, server-side imports) must move behind an API route. This is the biggest architectural shift.
⚠️ loading.tsxpendingComponent Next.js's loading.tsx convention maps to TanStack's route pendingComponent option, not a separate file. Same for error.tsxerrorComponent.
⚠️ Middleware + route handlers stay in Next.js TanStack Router is a client router. Middleware, route handlers (app/api/*), and server actions are orthogonal — keep them in whatever server layer you adopt.
⚠️ Dynamic imports + code splitting TanStack auto-code-splits by default. Your existing next/dynamic calls become unnecessary — delete them and let the router handle splitting per route.

Wisdom Lives in Communities

🌐 Where to test your migration skills Real edge cases live where other practitioners hit them. When you're stuck or want a sanity check: Post your migration plan, share your route tree, ask about the weird legacy route you can't rename. That's where parametric knowledge becomes wisdom.

Quick Quiz

1. What does the TanStack router plugin generate?

The route files
routeTree.gen.ts
vite.config.ts
The plugin watches routes/ and generates routeTree.gen.ts — the typed route tree that powers all type safety in navigation, params, and loaders.

2. Next.js page.tsx maps to which TanStack file?

route.tsx
index.tsx
layout.tsx
index.tsx is the exact-match leaf (like page.tsx). route.tsx is the layout (like layout.tsx).

3. Next.js layout.tsx maps to?

index.tsx
route.tsx
__root.tsx
route.tsx is a segment's layout wrapper — renders <Outlet/> for children. That's layout.tsx's role.

4. Which route should you convert first?

The root layout
The smallest leaf
The most complex one
Start with a tiny static leaf (like /about). One green route proves the pipeline end-to-end before you tackle complexity.

5. Next.js async server component becomes?

A client component
A loader + component
A route handler
Split the async server component into a route loader (fetches) + a component that reads via useLoaderData().

6. Where do loaders run in plain TanStack Router?

On the server
On the client
In a web worker
Plain TanStack Router runs loaders on the client. Server-only data (DB secrets, server imports) must move behind an API route. (TanStack Start adds SSR — out of scope.)

7. Next.js loading.tsx maps to which option?

pendingRoute
pendingComponent
suspenseFallback
loading.tsx → route's pendingComponent option. Same for error.tsxerrorComponent. Options, not separate files.

8. Which Next.js feature stays during migration?

File-based routing
Route handlers + middleware
App Router layouts
TanStack Router is a client router. API route handlers, middleware, and server actions live in your server layer — they're orthogonal and stay put.

9. Highest-leverage refactor in step 6?

Renaming files
useState → search params
Deleting comments
Moving shareable UI state (filters, tabs, pagination) from useState to URL search params gives shareable URLs and natural back-button for free.

10. Where do real migration edge cases get answered?

In this lesson only
TanStack Discord + GitHub
In the route tree file
Wisdom lives in communities. The TanStack Discord #router-questions and GitHub Discussions hold the migration war stories and edge cases no lesson can predict.
← Lesson 7: Loaders Cheat Sheet →
🎓 Your turn.
Pick a real Next.js app you own. Tell your teacher which route you'll convert first, and what surprised you when you did. Bring back edge cases — they become the next learning records.

Primary source: TanStack Router — Installation for the Vite plugin setup, then your own codebase. The migration is the curriculum.