Build Next.js 14 App Router routes with Server and Client Components. Use for creating pages, layouts, API routes, route handlers, dynamic segments, and implementing server-first rendering patterns.
Next.js 14 App Router patterns for server-first, type-safe routing with optimal performance.
app/
├── page.tsx # /
├── about/page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/page.tsx # /blog/:slug
├── dashboard/
│ ├── layout.tsx # Shared layout
│ ├── page.tsx # /dashboard
│ └── settings/page.tsx # /dashboard/settings
└── api/
└── users/route.ts # /api/users
// app/page.tsx
export default async function HomePage() {
// Fetch data server-side
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<main>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</main>
);
}
Benefits:
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Use when you need:
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// Generate static params at build time
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({
slug: post.slug,
}));
}
// app/shop/[category]/[product]/page.tsx
export default async function ProductPage({
params,
}: {
params: { category: string; product: string };
}) {
const product = await getProduct(params.category, params.product);
return <ProductDetails product={product} />;
}
// app/docs/[...slug]/page.tsx
export default async function DocsPage({
params,
}: {
params: { slug: string[] };
}) {
// /docs/a/b/c → params.slug = ['a', 'b', 'c']
const doc = await getDoc(params.slug);
return <Documentation doc={doc} />;
}
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>Global Navigation</nav>
</header>
{children}
<footer>Global Footer</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<aside>
<Sidebar />
</aside>
<main className="flex-1">{children}</main>
</div>
);
}
// app/(protected)/layout.tsx
import { auth } from '@clerk/nextjs';
import { redirect } from 'next/navigation';
export default async function ProtectedLayout({
children,
}: {
children: React.ReactNode;
}) {
const { userId } = auth();
if (!userId) {
redirect('/sign-in');
}
return <>{children}</>;
}
Organize routes without affecting URL structure:
app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── page.tsx # /
│ └── about/page.tsx # /about
└── (app)/
├── layout.tsx # App layout
└── dashboard/
└── page.tsx # /dashboard
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const posts = await db.posts.findMany();
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.posts.create({
data: {
title: body.title,
content: body.content,
},
});
return NextResponse.json(post, { status: 201 });
}
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await db.posts.findUnique({
where: { id: params.id },
});
if (!post) {
return new NextResponse('Not found', { status: 404 });
}
return NextResponse.json(post);
}
export async function PATCH(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const post = await db.posts.update({
where: { id: params.id },
data: body,
});
return NextResponse.json(post);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.posts.delete({
where: { id: params.id },
});
return new NextResponse(null, { status: 204 });
}
// app/dashboard/loading.tsx
export default function Loading() {
return <Spinner />;
}
// app/dashboard/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>Post Not Found</h2>
<p>The blog post you're looking for doesn't exist.</p>
</div>
);
}
// Trigger from page
import { notFound } from 'next/navigation';
const post = await getPost(params.slug);
if (!post) notFound();
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My App',
description: 'App description',
};
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
export default async function Page() {
// Fetch in parallel
const [user, posts] = await Promise.all([
getUser(),
getPosts(),
]);
return <Dashboard user={user} posts={posts} />;
}
export default async function Page() {
const user = await getUser();
const posts = await getPostsByUserId(user.id); // Depends on user
return <Dashboard user={user} posts={posts} />;
}
import { Suspense } from 'react';
export default function Page() {
return (
<>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</>
);
}
async function Posts() {
const posts = await getPosts(); // Slow query
return <PostsList posts={posts} />;
}
import Link from 'next/link';
export function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
);
}
'use client';
import { useRouter } from 'next/navigation';
export function LoginButton() {
const router = useRouter();
const handleLogin = async () => {
await login();
router.push('/dashboard');
};
return <button onClick={handleLogin}>Login</button>;
}
import { redirect } from 'next/navigation';
export default async function Page() {
const user = await getUser();
if (!user) {
redirect('/login');
}
return <Dashboard />;
}
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check authentication
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};
// app/dashboard/page.tsx
import { auth } from '@clerk/nextjs';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const { userId } = auth();
if (!userId) redirect('/sign-in');
const data = await getUserData(userId);
return <Dashboard data={data} />;
}
// app/dashboard/@analytics/page.tsx
export default async function Analytics() {
const data = await getAnalytics();
return <AnalyticsPanel data={data} />;
}
// app/dashboard/@team/page.tsx
export default async function Team() {
const members = await getTeamMembers();
return <TeamPanel members={members} />;
}
// app/dashboard/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<>
{children}
{analytics}
{team}
</>
);
}