The App Router is now the default in Next.js and has been production-ready long enough that clear patterns have emerged. Some approaches work well. Others cause real problems. This guide covers what to use, what to avoid, and why.
App Router vs Pages Router: The Current State
App Router is the default for all new Next.js projects. Pages Router is still fully supported and will not be removed anytime soon. If you have an existing Pages Router app that works, you do not need to migrate unless you specifically want App Router features.
For new projects, App Router is the correct choice. The data fetching model is cleaner, Server Components reduce client bundle size, and the layout system is more powerful. The learning curve is real but manageable.
Mixing both routers in the same project is possible but creates friction. The two routers have different conventions, different data fetching patterns, and different caching behaviors. If you are building a new project, pick one and stay with it.
Patterns That Work Well
Server Components for data fetching is the biggest win the App Router gives you. Instead of fetching in useEffect (which causes loading spinners and waterfall requests) or in getServerSideProps (which blocks the entire page), Server Components let you fetch data directly in the component that needs it.
// Server Component -- runs on server, no bundle cost
async function ProjectList({ orgId }: { orgId: string }) {
const projects = await db.collection("projects").find({ org_id: orgId }).toArray();
return <ul>{projects.map(p => <ProjectCard key={p._id} project={p} />)}</ul>;
}
This component never ships to the client. The database query runs on the server. No API route needed. No loading state needed for the initial render.
Route Handlers replace Pages Router API routes. Use them for endpoints that external services call (webhooks), for file uploads, for cases where you need full control over the response headers, or for endpoints consumed by client components via fetch. They are clean and straightforward.
Server Actions are best for form mutations. A form submits, the action runs on the server, and the page revalidates. No separate API route, no manual fetch call from the client. This works well for create, update, and delete operations on forms.
Parallel Routes solve a real problem: showing two different sections of a page that each have their own loading and error states. A dashboard with a sidebar and a main panel, where each section loads independently, is a good use case. The @slot convention takes getting used to but the result is clean.
Intercepting Routes are useful for modal patterns. You can show a photo in a modal when navigating from a gallery, then show the full photo page when navigating directly to it. This is a niche use case but solves it elegantly.
Patterns to Avoid
Client components wrapping everything defeats the purpose of the App Router. If your root layout is a client component, every component inside it runs on the client. You lose the Server Component benefits entirely. The rule: push the "use client" directive as far down the component tree as possible. Only components that actually need interactivity should be client components.
Mixing App and Pages Router unnecessarily creates cognitive overhead with no benefit. If you are adding a few new routes to an existing Pages Router app, consider whether you actually need App Router features for those routes. If not, add them to Pages Router and avoid the mental model switching.
Using Server Actions for everything is a mistake. Server Actions are great for form mutations. They are not great for complex API logic, for endpoints that need to return specific HTTP status codes, or for operations called programmatically from client code in non-form contexts. For those, use Route Handlers.
Fetching in useEffect for initial data is the old pattern. In the App Router, initial data should come from Server Components. useEffect data fetching is for data that changes after mount (user interactions, polling, real-time updates). Using it for initial page data causes the flash of empty content and the extra round trip that Server Components eliminate.
The Data Fetching Hierarchy
The correct order for thinking about where to fetch data:
- Server Component. Fetch directly, no API needed. Best for initial page data.
- Client Component with SWR or TanStack Query. Best for data that needs to refresh, depends on user interaction, or requires optimistic updates.
- useEffect with fetch. Acceptable only for side-effect-driven fetches (something happened, now load more data). Never for initial render data.
The most common mistake is skipping step 1 entirely because developers are familiar with step 3.
Caching in the App Router
Next.js 15 changed the fetch caching defaults. In Next.js 14, fetch calls were cached by default. In Next.js 15, they are not. This was a breaking change that caught many teams off guard.
The three caching tools you need to know:
fetch with cache option controls individual request caching. revalidatePath invalidates the cache for a specific route after a mutation. unstable_cache (now stable in practice, the name is historical) wraps any async function, not just fetch, with caching and revalidation.
For most apps, the pattern is: fetch data in Server Components without explicit caching (so it is always fresh on request), then revalidate specific routes after mutations. This is the simplest model and avoids most caching-related bugs.
If you need more aggressive caching for performance, add it explicitly with revalidation intervals. Start without it, measure, then add caching where the performance data shows it matters.
The Layout System
The layout system is genuinely better than Pages Router. Layouts persist across navigation (the layout does not remount when you navigate between routes that share the same layout). Nested layouts let you scope loading states and error boundaries to specific sections of the page.
The one gotcha: layouts cannot access route params from child routes. If your layout needs data that depends on a dynamic segment, you need to read the params in the layout directly, not try to pass them down from a page.
Keep Reading
- React Server Components: What They Are and When to Use Them -- the deep dive on the component model behind App Router
- Next.js Performance Optimization: The Practical Guide for Production Apps -- Core Web Vitals and bundle optimization
- Next.js Caching in 2026: The Complete Guide to All Four Layers -- understanding all four cache layers in detail
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace -- chat, projects, time tracking, AI meeting summaries, and invoicing -- in one tool. Try it free.