Loading...
Loading...
Next.js 15 App Router patterns: Server Components, Server Actions, data fetching, middleware. Trigger: When building Next.js apps, working with app router, server/client components, or API routes.
npx skill4agent add fearovex/claude-config nextjs-15// ✅ Server Component — async by default, no directive needed
async function UserProfile({ userId }: { userId: string }) {
const user = await db.users.findById(userId); // Direct DB access
return <ProfileCard user={user} />;
}
// ✅ Client Component — only when you need interactivity
'use client';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
await db.users.create({ name, email });
revalidatePath('/users');
redirect('/users');
}
// Direct usage in form
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Create</button>
</form>
);
}// lib/db.ts
import 'server-only'; // Build error if imported in client
export async function getSecretData() {
return db.secrets.findAll();
}// ✅ Parallel fetching in Server Component
async function Dashboard() {
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats(),
]);
return <DashboardView user={user} posts={posts} stats={stats} />;
}
// ✅ Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header /> {/* Immediate */}
<Suspense fallback={<PostsSkeleton />}>
<Posts /> {/* Streams when ready */}
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts(); // Waits here
return <PostList posts={posts} />;
}// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') ?? '1';
const users = await db.users.findMany({ page: parseInt(page) });
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.users.create(body);
return NextResponse.json(user, { status: 201 });
}// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};// Static
export const metadata = {
title: 'My App',
description: 'App description',
};
// Dynamic
export async function generateMetadata({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}app/
├── (auth)/ # Group with no URL impact
│ ├── layout.tsx # Layout only for auth pages
│ ├── login/page.tsx # /login
│ └── register/page.tsx # /register
├── (dashboard)/
│ ├── layout.tsx # Dashboard layout
│ └── overview/page.tsx # /overview
├── _components/ # Private folder (not a route)
├── layout.tsx # Root layout (required)
└── page.tsx # /// ❌ Unnecessary
'use client';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ Direct Server Component
async function UserList() {
const users = await db.users.findMany();
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}// ❌ Makes the entire tree client-side
'use client';
export default function Layout({ children }) { /* ... */ }
// ✅ Isolate the client component
export default function Layout({ children }) {
return <div><NavBar />{children}</div>; // NavBar can be 'use client'
}| Task | Pattern |
|---|---|
| DB in component | Server Component + async/await |
| Form | |
| Invalidate cache | |
| Redirect | |
| URL params | |
| Search params | |
| Protect routes | |
| Prevent client bundle | |
'use client''use client''use server'revalidatePathrevalidateTagimport 'server-only'