When Next.js 13 shipped the App Router, I was cautiously optimistic. The Pages Router had served us fine for years, but the new model promised server components, better data fetching, nested layouts. Sounds good on paper.
Six months ago we migrated a mid-sized SaaS product to the App Router. We didn’t do a full rewrite, we migrated incrementally, which Next.js actually supports. Here’s what I’d tell you if you asked me whether to switch.
· The Mental Model Shift is Real · Where the App Router Actually Wins · Where It Gets Messy · The Caching Story Is Complicated · Should You Migrate? · Enjoyed this?
The Mental Model Shift is Real
The biggest thing about the App Router isn’t the feature set, it’s the mental model. In the Pages Router, every file in /pages is a route. Components are client-side by default. Data fetching happens in getServerSideProps or getStaticProps. It’s predictable.
In the App Router, components are server-side by default. You explicitly opt into client behavior with ‘use client’. Data fetching happens inside async components. Layouts are nested. The whole paradigm flips.
// Pages Router - you fetch in a special function
export async function getServerSideProps() {
const data = await fetchSomething()
return { props: { data } }
}
// App Router - you just... fetch in the component
export default async function Page() {
const data = await fetchSomething()
return <div>{data.title}</div>
}
Honestly the App Router version feels cleaner to me now. But getting there took a solid few weeks of rewiring how I think about data flow.
Where the App Router Actually Wins
Nested layouts. This is the killer feature. In the Pages Router, if you wanted a different layout for /dashboard versus /dashboard/settings, you were dealing with _app.js gymnastics or per-page layout functions. In the App Router, you just put a layout.js file in the right folder.
app/
layout.js // root layout, always rendered
dashboard/
layout.js // dashboard layout, wraps all /dashboard routes
page.js
settings/
page.js
This alone was worth the migration for us. We had a complex multi-section admin panel and the nested layout model made it so much cleaner.
Server components also mean less JavaScript shipped to the client. Components that just render data from the database and don’t need interactivity don’t need to be included in the client bundle. That’s a real performance win.
Where It Gets Messy
The ‘use client’ boundary is where things get tricky. Once you add ‘use client’ to a component, all its children become client components too. This is fine until you start passing server components as children and things break in confusing ways.
The error messages are getting better, but six months ago they were rough. ‘You cannot use X in a Client Component’ with no clear path forward. Our team spent probably a collective week debugging component boundary issues.
Third-party libraries are also a pain point. A lot of packages assume they’re running in a browser (window, localStorage, etc.) and break in server components. You end up wrapping things in ‘use client’ wrappers just to use npm packages you were using fine before.
The Caching Story Is Complicated
In the Pages Router, caching was explicit: revalidate: 60, or getStaticProps, you knew what you were getting. In the App Router, there’s an aggressive default caching behavior that has caught a lot of people off guard. Fetches get cached by default.
// This is cached by default in App Router
const data = await fetch('https://api.example.com/posts')
// You have to explicitly opt out
const data = await fetch('https://api.example.com/posts', {
cache: 'no-store', // always fresh
})
// Or use revalidation
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
})
We shipped a bug in the first month because we expected data to be fresh and it wasn’t. Check your caching assumptions carefully.
Should You Migrate?
If you’re starting a new project, use the App Router. It’s clearly where Next.js is going, the DX is better once you internalize the model, and the performance benefits are real.
If you have an existing Pages Router app, I wouldn’t rush it. The incremental migration path works, but it’s not painless. Unless you have a specific need (nested layouts, server components for bundle size), the Pages Router still works perfectly well and will be supported for a long time.
The six months I spent was worth it for our specific use case. Your mileage may vary, and that’s an honest answer.
Enjoyed this?
If this saved you some headaches, consider following me on Medium. I write about Ruby, Rails, and JavaScript with a focus on real-world problems and honest takes.
Comments
Loading comments…