Next.js 13 introduced the App Router — the most significant architectural change in the framework's history. Built on React Server Components, the App Router fundamentally changes how data fetching, layouts, loading states, and error handling work in Next.js applications. For teams with existing Pages Router applications, the migration is substantial but the benefits — reduced client-side JavaScript, nested layouts, and streaming — make it worthwhile.
At StrikingWeb, we have migrated several production Next.js applications from the Pages Router to the App Router. This guide shares the practical knowledge we have gained — what changes, what breaks, and how to migrate incrementally without disrupting your production application.
What Changes in the App Router
The App Router introduces several new concepts that replace familiar Pages Router patterns.
Server Components by Default
In the App Router, every component is a React Server Component by default. Server Components render on the server and send HTML to the client — they do not ship any JavaScript to the browser. This means you can fetch data directly in your components using async/await, access backend resources (databases, file system, environment variables) without API routes, and reduce the JavaScript bundle size because server-only code stays on the server.
Components that need interactivity — event handlers, state, effects, or browser APIs — must be explicitly marked as Client Components with the 'use client' directive:
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
This explicit boundary between server and client code is the biggest mental model shift for developers coming from the Pages Router, where all components were client-side by default.
File-Based Routing with Conventions
The App Router uses special file conventions within the app/ directory:
page.tsx— Defines a route's unique UI. This replaces the default export from Pages Router fileslayout.tsx— Defines shared UI that wraps pages and child layouts. Layouts persist across navigation and do not re-renderloading.tsx— Creates instant loading states using React Suspense. Shows automatically while the page component loadserror.tsx— Creates error boundaries for a route segment. Catches errors and displays fallback UInot-found.tsx— Displays when a route is not matchedtemplate.tsx— Like layout but creates a new instance on each navigation (useful for animations)
Nested Layouts
One of the App Router's most powerful features is nested layouts. In the Pages Router, implementing persistent layouts required workarounds — wrapping components in getLayout functions or using custom _app.tsx patterns. In the App Router, layouts are a first-class primitive.
A root layout wraps the entire application. Section layouts — for a dashboard, settings panel, or marketing section — wrap their child routes. Each layout renders once and persists as the user navigates between child pages. This means navigation within a layout section does not re-render the layout, preserving state like scroll position, open modals, or active sidebar selections.
Data Fetching — The Big Change
The App Router replaces getServerSideProps, getStaticProps, and getInitialProps with a simpler model: fetch data directly in Server Components using async/await.
// app/products/[id]/page.tsx
async function ProductPage({ params }) {
const product = await fetch(
`https://api.example.com/products/${params.id}`
).then(res => res.json());
return (
<div>
<h1>{product.title}</h1>
<p>{product.description}</p>
</div>
);
}
This is dramatically simpler than the Pages Router equivalent, which required exporting a separate getServerSideProps function and passing data through props.
Caching and Revalidation
Next.js extends the native fetch API with caching and revalidation options:
fetch(url, { cache: 'force-cache' })— Cache the response indefinitely (equivalent togetStaticProps)fetch(url, { cache: 'no-store' })— Fetch fresh data on every request (equivalent togetServerSideProps)fetch(url, { next: { revalidate: 60 } })— Cache for 60 seconds then revalidate (equivalent to ISR)
For data sources that are not HTTP-based — database queries, file system reads, or third-party SDKs — the unstable_cache function provides the same caching behavior.
Migration Strategy
The most important thing to know about migrating to the App Router is that you do not have to do it all at once. The Pages Router and App Router coexist in the same Next.js project. You can migrate one route at a time, running both routers simultaneously until the migration is complete.
Phase 1: Set Up the App Directory
Create the app/ directory alongside your existing pages/ directory. Add a root layout with the HTML structure, metadata, and global providers that currently live in _app.tsx and _document.tsx:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Phase 2: Migrate Simple Pages First
Start with the simplest pages — static content pages, about pages, or marketing pages that do not have complex data fetching or interactivity. This lets your team learn the App Router patterns with minimal risk. Move the page from pages/about.tsx to app/about/page.tsx, convert data fetching from getStaticProps to direct fetching in the Server Component, and extract interactive elements into Client Components with the 'use client' directive.
Phase 3: Migrate Data-Heavy Pages
Next, tackle pages with significant data fetching — product listings, dashboards, or search results. The key decisions here involve choosing the right caching strategy for each data source, deciding which components need to be Client Components, and implementing loading states using loading.tsx or Suspense boundaries.
Phase 4: Migrate Complex Interactive Pages
Finally, migrate pages with heavy client-side interactivity — forms, real-time updates, or complex state management. These pages often require the most Client Components and may not see significant JavaScript reduction. However, they still benefit from nested layouts and improved loading states.
The migration to the App Router is not a weekend project for a production application. Plan for weeks, not days, and migrate incrementally. The ability to run both routers simultaneously is the most valuable feature of the migration path — use it.
Common Migration Challenges
Third-Party Libraries
Many React libraries use hooks, context, or browser APIs — all of which require Client Components. Wrap these libraries in Client Component boundaries. A common pattern is creating wrapper components that isolate the 'use client' directive:
'use client';
import { Analytics } from 'third-party-analytics';
export function AnalyticsProvider({
children,
}: {
children: React.ReactNode;
}) {
return <Analytics>{children}</Analytics>;
}
Global State Management
State management libraries like Redux, Zustand, and Jotai operate on the client side. In the App Router, these must be wrapped in Client Components. Consider whether server-side data fetching can replace some of your client-side state — if data is fetched on the server and passed to components, you may not need a global store for that data.
API Routes
API routes move from pages/api/ to app/api/ and use a new Route Handler syntax with exported HTTP method functions instead of a single handler. The new syntax is cleaner but requires rewriting each API route.
Performance Benefits
The App Router's Server Components deliver measurable performance improvements. In our migrations, we have seen initial JavaScript bundle reductions of 30-50 percent as server-only code no longer ships to the client. Time to Interactive improves because less JavaScript needs to be parsed and executed. And streaming SSR with Suspense boundaries means the browser receives and displays content progressively rather than waiting for the entire page to render.
Should You Migrate?
For new projects, the App Router is the clear choice — it is the future of Next.js and provides a better developer experience and better performance out of the box. For existing Pages Router applications, the decision depends on your priorities.
Migrate if you need nested layouts, want to reduce client-side JavaScript, or are building new features that benefit from Server Components. Delay migration if your application is stable, your team does not have bandwidth for the learning curve, or you depend heavily on libraries that have not yet adapted to Server Components.
At StrikingWeb, we help teams evaluate and execute App Router migrations. Whether you need guidance on migration strategy, hands-on development support, or a complete rebuild on the new architecture, our experience with production migrations ensures a smooth transition.