DEV Community

Abubakar Farooq
Abubakar Farooq

Posted on

What Broke When I Moved Client Projects to the Next.js App Router

Moving client projects from the Pages Router to the Next.js App Router sounded like a routine upgrade — until it wasn't. Every migration I've done has surfaced the same handful of breakages. Here are the four that cost me real hours, and how I fix them now.

1. getServerSideProps doesn't exist anymore

The first thing that breaks is data fetching. There is no getServerSideProps in the App Router — server components fetch directly:

// app/dashboard/page.jsx
export default async function Dashboard() {
  const res = await fetch("https://api.example.com/stats", {
    cache: "no-store", // opt out of the default static caching
  });
  const stats = await res.json();
  return <StatsGrid stats={stats} />;
}
Enter fullscreen mode Exit fullscreen mode

The gotcha: fetch results are cached by default in server components. During one migration, a client's admin dashboard kept showing yesterday's numbers until I added cache: "no-store". Whenever you port an SSR page, double-check what caching behavior you actually want.

2. The "use client" boundary keeps moving

Every component is a server component by default. The moment a server component imports a client-only dependency — a chart library, a drag-and-drop widget — the build fails. The fix is a "use client" directive, but push it as far down the tree as possible so the rest of the page still renders on the server:

// app/dashboard/ChartCard.jsx
"use client";

import { LineChart } from "some-chart-library";

export function ChartCard({ data }) {
  return <LineChart data={data} />;
}
Enter fullscreen mode Exit fullscreen mode

On one project we slapped "use client" on an entire page and silently gave back the performance gains the migration was supposed to deliver. Keep the directive on leaf components.

3. The router API changed shape

useRouter from next/router is now useRouter from next/navigation, and router.query is gone. You read route params with useParams(), search params with useSearchParams(), and <Head> is replaced by the metadata API:

// app/blog/[slug]/page.jsx
export const metadata = {
  title: "Blog post",
  description: "A migrated blog post",
};

export default async function Post({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <article>{post.title}</article>;
}
Enter fullscreen mode Exit fullscreen mode

Note that in recent Next.js versions params is a promise you have to await — another thing that broke a migration silently. I keep a checklist of router imports and search-replace them before the first build.

4. Auth redirects need a rethink

Pages Router auth helpers that relied on req/res don't map one-to-one onto the App Router. Session checks move into server components, and redirecting an unauthenticated user looks like this:

// app/admin/page.jsx
import { redirect } from "next/navigation";

export default async function AdminPage() {
  const session = await getSession();
  if (!session) redirect("/login");
  return <AdminPanel user={session.user} />;
}
Enter fullscreen mode Exit fullscreen mode

The takeaway

The App Router migration is worth it — server components, streaming, and colocated data fetching genuinely simplify client projects. But treat it as a rewrite of your routing and data layer, not a find-and-replace. Migrate route by route, keep "use client" boundaries small, and verify caching behavior on every page you port.

I build production Next.js apps and AI features for clients — more of my work is at theabubakar.dev.

Top comments (1)

Collapse
 
launchgatecheck profile image
Launch Gate •

Good list. The "use client" on the whole page is the most common one I see too, and it's silent: the build passes and you only notice in bundle size.

One version note on #1, because it catches people going the other direction: the default flipped in Next.js 15. fetch in server components is no longer cached by default there (nextjs.org/blog/next-15). So on 14 you need cache: "no-store" to get fresh data, and on 15+ you need cache: "force-cache" or next: { revalidate: N } if you want caching. Someone following a 14-era migration guide on 15 can end up with every request hitting the API.

Worth putting the Next version at the top of the migration checklist for that reason.