| name | nextjs |
| displayName | Next.js |
| description | Next.js 15+ App Router development patterns including Server Components, Client Components, data fetching, layouts, and server actions. Use when creating pages, routes, layouts, components, API route handlers, server actions, loading states, error boundaries, or working with Next.js navigation and metadata. |
| version | 1.0.0 |
Next.js Development Guidelines
Development patterns for Next.js 15+ using the App Router, Server Components, and modern data fetching.
Core Principles
- Server-First Architecture: Default to Server Components, use Client Components only when needed
- File-Based Routing: Use App Router conventions for pages, layouts, and route handlers
- Data Fetching: Fetch data where it's needed using async/await in Server Components
- Type Safety: Leverage TypeScript for route params, search params, and data types
- Performance: Optimize with streaming, parallel data fetching, and static generation
App Router Structure
File Conventions
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error boundary
├── not-found.tsx # 404 page
├── posts/
│ ├── layout.tsx # Posts layout
│ ├── page.tsx # /posts
│ ├── [id]/
│ │ └── page.tsx # /posts/123
│ └── new/
│ └── page.tsx # /posts/new
└── api/
└── posts/
└── route.ts # API route handler
Page Component
import { getPosts } from '@/lib/api';
export const metadata = {
title: 'Posts',
description: 'Browse all blog posts'
};
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
Dynamic Routes
import { getPost } from '@/lib/api';
import { notFound } from 'next/navigation';
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}
export default async function PostPage({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Server vs Client Components
Server Components (Default)
Use for:
- Data fetching
- Accessing backend resources
- Keeping sensitive info on server
- Reducing client-side JavaScript
import { db } from '@/lib/db';
export default async function PostsPage() {
const posts = await db.post.findMany();
return <PostList posts={posts} />;
}
Client Components
Use for:
- Event listeners (onClick, onChange, etc.)
- State and lifecycle (useState, useEffect)
- Browser-only APIs
- Custom hooks
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function SearchBar() {
const [query, setQuery] = useState('');
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
router.push(`/search?q=${query}`);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search posts..."
/>
<button type="submit">Search</button>
</form>
);
}
Composition Pattern
import { getPosts } from '@/lib/api';
import { SearchBar } from '@/components/SearchBar';
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<SearchBar /> {/* Client Component for interactivity */}
<PostList posts={posts} /> {/* Can be Server Component */}
</div>
);
}
Data Fetching
Basic Pattern
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
}).then(res => res.json());
return <PostList posts={posts} />;
}
Parallel Data Fetching
export default async function DashboardPage() {
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<Stats data={stats} />
</div>
);
}
Streaming with Suspense
import { Suspense } from 'react';
export default function PostsPage() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts();
return <PostList posts={posts} />;
}
Layouts
Root Layout (Required)
import './globals.css';
export const metadata = {
title: {
default: 'My Blog',
template: '%s | My Blog'
}
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}
Nested Layout
export default function PostsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<aside>
<PostsSidebar />
</aside>
<div>{children}</div>
</div>
);
}
Route Handlers (API Routes)
Basic Handler
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const posts = await getPosts();
return NextResponse.json({ posts });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json({ post }, { status: 201 });
}
Dynamic Route Handler
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id);
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json({ post });
}
Server Actions
Basic Server Action
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const post = await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
}
Using in Forms
import { createPost } from '@/app/actions/posts';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}
Navigation
Link Component
import Link from 'next/link';
export function PostCard({ post }: { post: Post }) {
return (
<Link href={`/posts/${post.id}`} prefetch={true}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</Link>
);
}
Programmatic Navigation
'use client';
import { useRouter } from 'next/navigation';
export function PostActions({ postId }: { postId: string }) {
const router = useRouter();
const handleDelete = async () => {
await deletePost(postId);
router.push('/posts');
router.refresh();
};
return <button onClick={handleDelete}>Delete</button>;
}
Router Hooks
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
const pathname = usePathname();
const searchParams = useSearchParams();
const query = searchParams.get('q');
Metadata
export const metadata = {
title: 'All Posts',
description: 'Browse our collection of blog posts'
};
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}
Error Handling
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
export default function NotFound() {
return <div>Post Not Found</div>;
}
Static Generation & ISR
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ id: post.id }));
}
export const revalidate = 3600;
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const post = await getPost(id);
return <Post data={post} />;
}
Additional Resources
For detailed information, see: