What Is the App Router?
The App Router, introduced in Next.js 13 and now the default in Next.js 16, is a file-system based router built on top of React Server Components. It lives in the app/\ directory and replaces the older pages/\ router as the recommended approach for all new projects.
If you're still building with pages/\, the App Router isn't just a new feature — it's a fundamentally different mental model. Understanding it properly will make you a significantly more effective Next.js developer.
Server Components vs. Client Components
This is the most important concept in the App Router. Every component in the app/\ directory is a React Server Component (RSC) by default.
Server Components
Server components render on the server and send HTML to the browser. They can:
- Fetch data directly using
async/await\ - Access server-only resources (databases, environment variables)
- Import server-only packages without sending them to the client
\tsx
// app/blog/page.tsx — runs entirely on the server
export default async function BlogPage() {
const posts = await db.query("SELECT * FROM posts");
return <PostList posts={posts} />;
}
\\
Because server components never ship JavaScript to the browser, they reduce your bundle size dramatically. No useEffect\, no hydration overhead.
Client Components
Client components are the ones you're used to from React. They run in the browser and can use hooks, event listeners, and browser APIs. To opt into client rendering, add "use client"\ at the top of the file:
\`tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return setCount(c => c + 1)}>{count};
}
`\
When to Use "use client"
A common mistake is marking too many components as client components. Use "use client"\ only when the component needs:
- React state (
useState\,useReducer\) - Effects (
useEffect\,useLayoutEffect\) - Browser APIs (
window\,document\,localStorage\) - Event listeners
- Third-party libraries that require a browser environment
Pass server-fetched data down to client components as props. Keep the boundary as close to the leaf as possible.
The params Promise API (Breaking Change in Next.js 15+)
This is the biggest breaking change you'll encounter when upgrading. In Next.js 15 and 16, params\ and searchParams\ in page components are now Promises, not plain objects.
Before (Next.js 14)
\tsx
// Old — params is a plain object
export default function PostPage({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}
\\
After (Next.js 15+)
\tsx
// New — params is a Promise
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}
\\
This change enables better streaming and parallel data fetching. It affects every dynamic route in your app, so update all page components when migrating.
generateStaticParams
For dynamic routes, generateStaticParams\ tells Next.js which paths to pre-render at build time. This is the App Router equivalent of getStaticPaths\:
\tsx
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
\\
Any path not returned by generateStaticParams\ will be rendered on-demand (or return 404, depending on your dynamicParams\ setting). For blogs and docs, always use generateStaticParams\ — pre-rendered pages are faster and reduce server load.
generateMetadata
Dynamic metadata for SEO is handled by generateMetadata\, an async function that runs on the server:
\`tsx
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetchPost(slug);
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
images: [post.ogImage],
},
};
}
`\
This gives every page unique, accurate metadata without client-side workarounds. Combined with static generation, it produces near-perfect SEO scores.
File Conventions: layout, loading, error
The App Router introduces special file names that automatically wrap your pages:
layout.tsx
Layouts wrap pages and persist across navigations. The root layout is required and must include <html>\ and <body>\ tags:
\tsx
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
\\
Nested layouts let you share UI across groups of pages without re-rendering on navigation.
loading.tsx
Create a loading.tsx\ file to show a loading UI while a page or layout is fetching data. Next.js wraps the page in a <Suspense>\ boundary automatically:
\tsx
// app/blog/loading.tsx
export default function Loading() {
return <div className="animate-pulse">Loading posts...</div>;
}
\\
error.tsx
An error.tsx\ file catches runtime errors in that segment and displays a fallback UI. It must be a client component:
\`tsx
"use client";
export default function Error({ reset }: { reset: () => void }) {
return (
Something went wrong.
Try again
);
}
`
\
Performance Tips
1. Fetch data at the component level. Server components can fetch their own data in parallel. No more prop-drilling data through component trees.
2. Use React.cache\ to deduplicate requests. If multiple components fetch the same resource, wrap the fetch in cache()\ to avoid redundant database calls.
3. Prefer static over dynamic rendering. Static pages are faster, cheaper, and cached by default. Only use dynamic = "force-dynamic"\ when the page genuinely needs fresh data on every request.
4. Stream with Suspense. Wrap slower data-fetching components in <Suspense>\ so fast parts of the page render immediately while slower parts load in the background.
5. Optimize images with next/image\. It handles lazy loading, resizing, and format conversion automatically. Never use plain <img>\ tags in production.
Putting It All Together
The App Router might feel like a lot to take in, but the mental model is consistent: server components by default, client components only when you need interactivity, and file conventions to handle loading, error, and layout states.
All Craftly Next.js templates are built with these patterns baked in — proper layout nesting, generateStaticParams\ for dynamic routes, generateMetadata\ for SEO, and "use client"\ used sparingly at the leaf level. It's the best way to see these patterns in production-quality code.
If you want to accelerate your Next.js development, check out Craftly's template collection — every template is built with Next.js 16, TypeScript, and App Router best practices from the start.
Originally published on Craftly.
Check out our premium templates:
- SaaSify — SaaS Landing Page ($49)
- Developer Portfolio ($39)
- Blog Template ($29)
- Pricing Page ($19)
Built with Next.js 16, TypeScript, and Tailwind CSS v4.
Top comments (0)