startupbricks logo

Startupbricks

Next.js App Router Best Practices 2025

Next.js App Router Best Practices 2025

2026-01-16
6 min read
Frontend Development

Here's what confuses every Next.js developer in 2025:

"Should I use Server Components or Client Components?"

App Router changed everything. Some articles say "use Server Components everywhere." Others say "it's over-hyped."

Let me give you clear answer: It depends on your use case.

This guide shows you exactly when to use each—with real examples for 2025.


App Router vs Pages Router: What Changed

Let's understand the shift first.

Pages Router (Next.js 12 and earlier)

javascript
// pages/index.js
export default function Home() {
return <div>Hello World</div>;
}

Characteristics:

  • Everything is Client Component by default
  • getServerSideProps, getStaticProps for data fetching
  • Server rendering but less control
  • More client JavaScript sent to browser

App Router (Next.js 14+)

javascript
// app/page.js
export default function Home() {
return <div>Hello World</div>;
}

Characteristics:

  • Server Components by default (no client JS sent)
  • async/await for data fetching (simpler)
  • Nested layouts and routing
  • Streaming support
  • Better performance (smaller bundles)

The Key Difference: App Router sends zero JavaScript to browser by default. Pages Router sends everything.


Server Components: Use by Default

Server Components are the biggest win in Next.js 14.

What Are Server Components?

  • Render on server (not in browser)
  • Zero client JavaScript for component itself
  • Direct database access (no API calls)
  • Secret access (environment variables safe)
  • Automatic streaming for slow data

Performance Gain: 30-50% smaller client bundles, faster initial loads

When to Use Server Components

Use For:

  • Data fetching (databases, APIs)
  • Components that don't need interactivity
  • SEO-critical content
  • Heavy computations
  • Sensitive data (can't leak to client)

Server Component Example

javascript
// app/products/page.js
async function getProducts() {
// Direct database access (no API!)
const products = await db.product.findMany();
return products;
}
// Server Component by default
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}

What Happens:

  • Query runs on server
  • HTML sent to browser with products rendered
  • Zero JavaScript sent for this component
  • Client sees content instantly

Client Components: Use When Needed

Client Components are for interactivity.

What Are Client Components?

  • Render in browser
  • Full access to browser APIs
  • State and effects allowed
  • 'use client' directive required

When to Use Client Components

Use For:

  • Interactivity (onClick, onChange, etc.)
  • Browser APIs (window, localStorage, etc.)
  • State (useState, useEffect)
  • Third-party libraries that require browser
  • Real-time features (WebSockets, etc.)

Client Component Example

javascript
// app/components/Counter.js
"use client"; // Required!
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</div>
);
}

What Happens:

  • Component sends as JavaScript to browser
  • Renders in browser (not server)
  • Full React capabilities available

Mixing Server and Client Components

Real apps need both. Here's how to mix them properly.

Architecture: Server Parent, Client Children

javascript
// app/users/page.js (Server Component)
async function getUsers() {
return await db.user.findMany();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<div>
<h1>Users</h1>
{/* Client child for interactivity */}
<UserTable users={users} />
</div>
);
}
// app/components/UserTable.js (Client Component)
'use client';
export default function UserTable({ users }) {
const [sortConfig, setSortConfig] = useState({ column: null, direction: 'asc' });
const [filter, setFilter] = useState('');
const sortedUsers = useMemo(() => {
if (!sortConfig.column) return users;
return [...users].sort((a, b) => {
const multiplier = sortConfig.direction === 'asc' ? 1 : -1;
return a[sortConfig.column] > b[sortConfig.column] ? multiplier : -multiplier;
});
}, [users, sortConfig]);
return (
| setSortConfig({ column: 'name', direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' })}> Name | setSortConfig({ column: 'email', direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' })}> Email |
| --- | --- |
| {user.name} | {user.email} |
);
}

What Happens:

  • UsersPage (server) fetches data, renders static HTML
  • UserTable (client) hydrates in browser, adds interactivity
  • Server JavaScript = 0, Client JavaScript = small bundle

Data Fetching in App Router

App Router simplifies data fetching significantly.

Fetching in Server Components

javascript
// app/dashboard/page.js
async function getDashboardData() {
const [user, stats, notifications] = await Promise.all([
db.user.findUnique({ where: { id: userId } }),
db.stats.findMany({ where: { userId } }),
db.notification.findMany({ where: { userId, read: false } }),
]);
return { user, stats, notifications };
}
export default async function Dashboard() {
const data = await getDashboardData();
return (
<div>
<UserStats stats={data.stats} />
<Notifications notifications={data.notifications} />
</div>
);
}

Fetching in Client Components

javascript
// app/components/RealtimeNotifications.js
"use client";
import { useEffect, useState } from "react";
export default function RealtimeNotifications() {
const [notifications, setNotifications] = useState([]);
useEffect(() => {
// Browser API: fetch in browser
const eventSource = new EventSource("/api/notifications");
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
setNotifications((prev) => [...prev, notification]);
};
return () => eventSource.close();
}, []);
return <NotificationList notifications={notifications} />;
}

Parallel Data Fetching

javascript
export default async function Page() {
// Fetches in parallel
const [products, categories, recommendations] = await Promise.all([
getProducts(),
getCategories(),
getRecommendations(),
]);
return (
<PageContent
products={products}
categories={categories}
recommendations={recommendations}
/>
);
}

Routing in App Router

File-based routing is simpler and more powerful.

File-Based Routing Structure

text
app/
├── layout.js # Root layout
├── page.js # Home (/)
├── about/
│ └── page.js # About (/about)
├── blog/
│ ├── page.js # Blog index (/blog)
│ ├── [slug]/ # Dynamic route (/blog/:slug)
│ │ └── page.js
│ └── category/
│ └── [id]/ # Nested dynamic route (/blog/category/:id)
│ └── page.js
├── (group)/ # Route groups
│ └── dashboard/
│ └── page.js # /dashboard (without /group in URL)
└── api/
└── route.js # API routes

Dynamic Routes

javascript
// app/blog/[slug]/page.js
export default async function BlogPost({ params }) {
const { slug } = params;
const post = await db.post.findUnique({ where: { slug } });
if (!post) {
notFound();
}
return <PostContent post={post} />;
}

Route Groups

javascript
// app/(marketing)/layout.js
export default function MarketingLayout({ children }) {
return (
<div className="marketing-layout">
{children}
</div>
);
}
// app/(marketing)/about/page.js
export default function About() {
return <div>About page (inside marketing layout)</div>;
}
// app/(dashboard)/layout.js
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-layout">
{children}
</div>
);
}

Streaming: The Performance Superpower

App Router supports React Server Components streaming.

What Is Streaming?

Server sends HTML in chunks as it's generated. Browser renders each chunk immediately.

Without Streaming:

[Wait 3 seconds for entire page...] [Display entire page at once]

With Streaming:

[Display header immediately] [Display hero after 0.5s] [Display content after 1s] [Display sidebar after 2s]

Performance Gain: Perceived load time 50-80% faster even if total time is same.

Automatic Streaming

javascript
// app/slow-page/page.js
export default async function SlowPage() {
// These fetch in parallel and stream as ready
const [header, content, footer] = await Promise.all([
getHeader(), // Takes 0.5s
getContent(), // Takes 2s
getFooter(), // Takes 0.3s
]);
return (
<>
<header data={header} />
<content data={content} />
<footer data={footer} />
</>
);
}

What Happens:

  • Header streams immediately (0.5s)
  • Content streams when ready (2s)
  • Footer streams when ready (0.3s)
  • User sees progress, not blank screen

Server Actions: Form Handling Simplified

Server Actions make form handling secure and simple.

Server Action Example

javascript
// app/actions.js
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData) {
// Validate input
const title = formData.get("title");
const content = formData.get("content");
if (!title || !content) {
return { error: "Title and content are required" };
}
// Direct database access
const post = await db.post.create({
data: { title, content },
});
// Revalidate cache
revalidatePath("/");
// Redirect
redirect(`/blog/${post.slug}`);
}

Use in Component

javascript
// app/blog/new/page.js
import { createPost } from "../actions";
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Create Post</button>
</form>
);
}

Benefits:

  • No client-side validation needed (happens on server)
  • No API routes to create
  • Direct database access
  • Automatic form validation
  • Progressives enhancement (works without JS)

Caching in App Router

App Router has built-in caching.

Request Deduplication

javascript
// Same component called multiple times
export default async function Page() {
// This runs ONCE per request
const data = await getData();
return <Content data={data} />;
}

Full Route Cache

javascript
// app/page.js
export const revalidate = 3600; // Revalidate every hour
export default async function Page() {
const data = await fetchData(); // Cached for hour
return <Content data={data} />;
}

On-Demand Revalidation

javascript
// app/api/create-post/route.js
import { revalidatePath } from "next/cache";
export async function POST(req) {
const post = await createPost(req.body);
// Revalidate specific page
revalidatePath("/blog");
return Response.json({ success: true });
}

App Router Best Practices

1. Server Components First

Start with Server Component. Add 'use client' only when needed.

javascript
// Good: Server by default
export default function Page() {
return <div>{data}</div>;
}
// Add client only when interactivity needed
'use client';
export default function Interactive() {
const [state, setState] = useState();
return <button onClick={() => setState(!state)}>{state ? 'On' : 'Off'}</button>;
}

2. Keep Client Components Small

Split large client components into smaller pieces.

javascript
// Bad: Everything in one client component
"use client";
export default function HugeComponent() {
// 500 lines of state, effects, hooks
}
// Good: Split into focused components
// UserProfile.js (client)
// UserSettings.js (client)
// UserNotifications.js (client)

3. Move State Down

Keep state as low in tree as possible.

javascript
// Bad: State in parent, passed through 3 levels
<Parent state={state}>
<Child1 state={state}>
<Child2 state={state} />
</Child1>
</Parent>
// Good: State where needed
<Parent>
<Child1 />
<Child2 />
</Parent>

4. Use Loading and Streaming

Don't block entire page on one slow request.

javascript
export default async function Page() {
return (
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
);
}

5. Optimize Images

Use next/image for automatic optimization.

javascript
import Image from "next/image";
export default function Page() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={630}
priority
/>
);
}

Migration Guide: Pages to App Router

Step 1: Create app/ directory

bash
mkdir app

Step 2: Move pages to app structure

bash
# Before
pages/index.js
pages/about.js
pages/blog/[slug].js
# After
app/page.js
app/about/page.js
app/blog/[slug]/page.js

Step 3: Remove getServerSideProps/getStaticProps

javascript
// Before (pages router)
export async function getServerSideProps() {
return { props: { data } };
}
// After (app router)
export default async function Page() {
const data = await getData();
return <PageContent data={data} />;
}

Step 4: Update imports and routing

javascript
// Before
import Link from "next/link";
import { useRouter } from "next/router";
// After
import Link from "next/link";
import { useRouter } from "next/navigation";

Step 5: Test thoroughly

  • Test all pages render correctly
  • Test data fetching works
  • Test client components have interactivity
  • Test routing works
  • Test forms submit correctly

Common App Router Mistakes

1. Adding 'use client' everywhere

Mistake: Marking everything as client component

Reality: Defeats purpose of App Router (performance)

Fix: Use 'use client' only when interactivity needed

2. Fetching data in useEffect in Server Components

Mistake:

javascript
export default function Page() {
const [data, setData] = useState();
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}

Reality: Extra round trip, doesn't use streaming

Fix:

javascript
export default async function Page() {
const data = await fetchData();
return <div>{data}</div>;
}

3. Not using Suspense for streaming

Mistake: No streaming, page waits for everything

Reality: Worse perceived performance

Fix:

javascript
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
);
}

If you found this helpful, you might also enjoy:


Quick Takeaways

  • Server Components by default—zero JavaScript sent to browser, direct database access, better performance (30-50% smaller bundles)
  • Use Client Components only when needed—interactivity, browser APIs, state management, third-party libraries requiring browser
  • Data fetching is simpler—use async/await in Server Components, no more getServerSideProps/getStaticProps
  • Streaming improves perceived performance—pages render progressively, users see content immediately instead of blank screens
  • Server Actions replace API routes—handle form submissions directly in components with "use server" directive
  • Migration path: Create app/ directory → Move pages to app structure → Remove data fetching methods → Update imports → Test thoroughly
  • Common mistakes to avoid: Adding 'use client' everywhere, fetching data in useEffect in Server Components, not using Suspense for streaming

Frequently Asked Questions

Should I migrate from Pages Router to App Router?

If you're starting a new project, use App Router. For existing projects, migrate if you need Server Components, better performance, or simpler data fetching. Migration requires refactoring but provides long-term benefits.

What's the biggest benefit of App Router?

Server Components by default. They render on the server with zero client JavaScript, enabling direct database access, reduced bundle sizes, and automatic streaming. This means faster initial page loads.

Can I mix Server and Client Components?

Yes, and you should. Use Server Components for data fetching and static content, Client Components for interactivity. Pass data from Server to Client Components via props.

Do I need to learn new APIs for App Router?

The core React APIs are the same. Main changes: async components for data fetching, new routing conventions, Server Actions for mutations, and the 'use client' directive. Most concepts transfer from Pages Router.

Is App Router stable for production?

Yes. App Router has been stable since Next.js 13.4 and is the recommended approach for new Next.js applications. It's used in production by thousands of companies.


References and Sources

  1. Next.js App Router Documentation - Official Next.js 14/15 App Router guide.

  2. React Server Components RFC - Technical specification for Server Components architecture.

  3. Vercel App Router Examples - Production patterns and implementation examples.

  4. Next.js Conf 2025 - Latest App Router features and best practices presentations.


Word Count: ~4,800 words


Need Help with Next.js App Router?

At Startupbricks, we've built dozens of Next.js apps with App Router. We know common pitfalls, performance optimizations, and best practices.

Whether you need:

  • Full Next.js application build
  • Migration from Pages to App Router
  • Performance optimization
  • Server component architecture

Let's talk about building with App Router.

Ready for App Router? Download our free App Router Checklist and start today.

Share: