Next.js App Router: What Changed and Why It Matters

The Next.js Pages Router has served the community well since the beginning — file-based routing,
,1getServerSideProps
, and a simple1getStaticProps
folder for backend logic. But since Next.js 13, the framework has been moving toward a new paradigm: the App Router.1pages/api/
If you're still on the Pages Router (or just starting out), this post breaks down what actually changed, why it matters, and how to start using the new patterns today.
The Core Shift: React Server Components
The App Router is built on React Server Components (RSC). Every component in the
directory is a Server Component by default, which means:1app/
- It renders on the server and ships no JavaScript to the browser
- It can
directly — no1async/await
+1useEffect
dance to load data1useState - It can safely import Node.js modules, access databases, and read the filesystem
This flips the old model on its head. Instead of fetching at the page level with
and threading props down through every layer, you fetch right where the data is consumed.1getServerSideProps
Pages Router (old)
1// pages/posts/[slug].tsx 2export async function getServerSideProps(context) { 3 const { slug } = context.params; 4 const post = await fetchPostFromDB(slug); 5 return { props: { post } }; 6} 7 8export default function PostPage({ post }) { 9 return <article><h1>{post.title}</h1></article>; 10} 11
App Router (new)
1// app/posts/[slug]/page.tsx 2async function getPost(slug: string) { 3 const post = await fetchPostFromDB(slug); 4 return post; 5} 6 7export default async function PostPage({ params }: { params: { slug: string } }) { 8 const post = await getPost(params.slug); 9 return <article><h1>{post.title}</h1></article>; 10} 11
No export ceremony, no props threading — just
directly in the component.1async/await
New File Conventions
The
directory introduces special filenames that map to specific behaviors:1app/
| File | Purpose | |------|---------| |
| The UI for a route (replaces1page.tsx
) | |1pages/foo.tsx
| Persistent wrapper for a segment and its children | |1layout.tsx
| Automatic Suspense fallback while the page loads | |1loading.tsx
| Error boundary for the segment | |1error.tsx
| 404 UI for the segment | |1not-found.tsx
| API endpoint (replaces1route.ts
) |1pages/api/foo.ts
A typical blog structure looks like this:
1app/ 2 layout.tsx ← root layout, wraps every page 3 page.tsx ← home page (/) 4 posts/ 5 page.tsx ← posts list (/posts) 6 [slug]/ 7 page.tsx ← individual post (/posts/my-post) 8 loading.tsx ← shown while post data fetches 9
Layouts: No More 1_app.js
1_app.jsIn the Pages Router,
was the place to wrap every page with a shared shell. The App Router replaces this with nested1_app.js
files.1layout.tsx
1// app/layout.tsx 2export const metadata = { 3 title: 'My Blog', 4 description: 'Writing about React and beyond.', 5}; 6 7export default function RootLayout({ children }: { children: React.ReactNode }) { 8 return ( 9 <html lang="en"> 10 <body> 11 <nav>My Site</nav> 12 <main>{children}</main> 13 <footer>© 2026</footer> 14 </body> 15 </html> 16 ); 17} 18
Layouts are persistent — they don't remount when you navigate between pages in the same segment. This is great for sidebar state, scroll position, or any component that's expensive to re-initialize.
You can also nest layouts:
wraps only the1app/dashboard/layout.tsx
routes without affecting anything outside.1/dashboard/*
When to Use 1"use client"
1"use client"Server Components can't use browser APIs, React hooks (
,1useState
,1useEffect
), or event listeners. When you need any of those, add1useRef
as the first line of the file:1"use client"
1'use client'; 2 3import { useState } from 'react'; 4 5export function LikeButton({ postId }: { postId: string }) { 6 const [liked, setLiked] = useState(false); 7 8 return ( 9 <button onClick={() => setLiked(!liked)}> 10 {liked ? 'Liked' : 'Like'} 11 </button> 12 ); 13} 14
The pattern that works best: keep the page itself a Server Component for data fetching, then pass data down to small Client Components that handle interactivity. Push
as far down the tree as possible — this keeps the client bundle small.1"use client"
Route Handlers Replace API Routes
becomes1pages/api/hello.ts
with named HTTP method exports:1app/api/hello/route.ts
1// app/api/hello/route.ts 2import { NextResponse } from 'next/server'; 3 4export async function GET() { 5 return NextResponse.json({ message: 'Hello from the App Router' }); 6} 7 8export async function POST(request: Request) { 9 const body = await request.json(); 10 return NextResponse.json({ received: body }); 11} 12
Named exports per method make the intent clear and remove the
branching that cluttered old API routes.1if (req.method === 'POST')
Should You Migrate?
If you're starting a new project today, use the App Router — it's the default when you run
. For existing apps, migration can be incremental: the1create-next-app
and1app/
directories can coexist in the same project while you transition route by route.1pages/
Start with the routes that benefit most:
- Pages with heavy server-side data fetching (RSC eliminates the
+ props threading)1getServerSideProps - Any shared chrome currently in
(move it to1_app.js
)1app/layout.tsx - Pages where you want streaming —
gives you a Suspense boundary for free1loading.tsx
The App Router has a steeper learning curve than the Pages Router, but once the Server Component mental model clicks — the server is where data lives, the client is where interaction lives — the code becomes noticeably simpler and the bundle stays lean.
