Skip to content
maksim zaytsev
Writing

Jan 15, 2024 · 8 min

Notes on the Next.js 15 app router

I moved a mid-size dashboard app from the pages router to Next.js 15's app router over about three weeks, alongside regular feature work. These are the notes I wish I'd had going in.

What changed

The biggest shift is mental: routing is now a folder tree, and every folder can carry its own layout, loading state, and error boundary. getServerSideProps and getStaticProps are gone — data fetching happens directly in server components, as plain async functions. Route handlers replace API routes: one route.ts per segment instead of a single file with a switch on req.method.

Caching also changed shape. Fetches are cached by default unless you opt out, which is convenient until you forget it's happening and stare at stale data for twenty minutes. I now default new fetches to { cache: 'no-store' } and add caching back deliberately, not the other way around.

async function getInvoices(orgId: string) {
  const res = await fetch(`${API}/invoices?org=${orgId}`, {
    cache: 'no-store',
  });
  if (!res.ok) throw new Error('Failed to load invoices');
  return res.json();
}

What surprised me

Server components composing with client components is smoother than I expected — you pass server-fetched data down as props and only the interactive leaf becomes 'use client'. What surprised me less pleasantly: how easily a stray 'use client' at the top of a shared utility file drags half the tree into the client bundle. I found three of these by accident, checking the bundle analyzer.

useSearchParams also forces the enclosing component into client-only rendering, which is easy to miss when you're refactoring a page that used to read router.query for free.

What I'd do differently

I'd draw the server/client boundary on paper before writing code, not after. I'd also introduce route groups from day one instead of retrofitting them — reorganizing directories after routes are load-bearing production URLs is more nerve-wracking than it needs to be. And I'd write the loading.tsx files up front instead of as an afterthought; they change how the whole migration feels, because the app stops looking broken while data streams in.

Net: a good change, but the app router rewards deciding the shape of your server/client split before you start, not while you're mid-migration.