
Nextjs
- 56 installs
- 101 repo stars
- Updated November 28, 2025
- blencorp/claude-code-kit
Next.js is a Claude Code skill that provides Next.js 15+ App Router patterns for Server and Client Components, data fetching, layouts, and server actions.
About
Next.js is a skill that gives Claude Next.js 15+ App Router patterns for Server Components, Client Components, data fetching, layouts, and server actions. A developer uses it when creating pages, routes, layouts, API route handlers, loading states, or error boundaries. It defaults to a server-first architecture and file-based routing.
- Next.js 15+ App Router conventions for pages, layouts, and route handlers
- Server-first architecture: default Server Components, opt into Client Components
- Data fetching patterns including parallel fetching and streaming with Suspense
Nextjs by the numbers
- 56 all-time installs (skills.sh)
- Ranked #1,245 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
nextjs capabilities & compatibility
- Capabilities
- frontend · api development · ui design
- Use cases
- frontend · api development
- IDEs
- vscode · cursor ide
What nextjs says it does
Next.js 15+ App Router development patterns including Server Components, Client Components, data fetching, layouts, and server actions.
Development patterns for Next.js 15+ using the App Router, Server Components, and modern data fetching.
**Server-First Architecture**: Default to Server Components, use Client Components only when needed
npx skills add https://github.com/blencorp/claude-code-kit --skill nextjsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 101 |
| Last updated | November 28, 2025 |
| Repository | blencorp/claude-code-kit ↗ |
What it does
Build Next.js 15 App Router pages, layouts, route handlers, and server actions.
Who is it for?
Building Next.js 15 App Router pages, layouts, and server actions
Skip if: Pages Router or non-Next React apps
When should I use this skill?
Creating Next.js pages, routes, layouts, route handlers, or server actions
What you get
Pages and layouts follow server-first App Router conventions with correct data fetching.
- Pages
- Layouts
- Route handlers
By the numbers
- Next.js 15+ App Router file conventions
Files
Next.js Development Guidelines
Development patterns for Next.js 15+ using the App Router, Server Components, and modern data fetching.
Core Principles
1. Server-First Architecture: Default to Server Components, use Client Components only when needed 2. File-Based Routing: Use App Router conventions for pages, layouts, and route handlers 3. Data Fetching: Fetch data where it's needed using async/await in Server Components 4. Type Safety: Leverage TypeScript for route params, search params, and data types 5. 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 handlerPage Component
// app/posts/page.tsx
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
// app/posts/[id]/page.tsx
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
// app/posts/page.tsx (Server Component by default)
import { db } from '@/lib/db';
export default async function PostsPage() {
// Direct database access
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
// components/SearchBar.tsx
'use client'; // Required directive
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
// app/posts/page.tsx (Server Component)
import { getPosts } from '@/lib/api';
import { SearchBar } from '@/components/SearchBar'; // Client Component
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
// Server Component with async/await
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(res => res.json());
return <PostList posts={posts} />;
}Parallel Data Fetching
export default async function DashboardPage() {
// Fetch in parallel
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(); // Slow data fetch
return <PostList posts={posts} />;
}Layouts
Root Layout (Required)
// app/layout.tsx
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
// app/posts/layout.tsx
export default function PostsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<aside>
<PostsSidebar />
</aside>
<div>{children}</div>
</div>
);
}Route Handlers (API Routes)
Basic Handler
// app/api/posts/route.ts
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
// app/api/posts/[id]/route.ts
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
// app/actions/posts.ts
'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(); // Refresh server components
};
return <button onClick={handleDelete}>Delete</button>;
}Router Hooks
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
const pathname = usePathname(); // /posts/123
const searchParams = useSearchParams(); // ?q=hello
const query = searchParams.get('q');Metadata
// Static metadata
export const metadata = {
title: 'All Posts',
description: 'Browse our collection of blog posts'
};
// Dynamic metadata
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}Error Handling
// app/posts/error.tsx
'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>
);
}
// app/posts/[id]/not-found.tsx
export default function NotFound() {
return <div>Post Not Found</div>;
}Static Generation & ISR
// Generate static pages at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ id: post.id }));
}
// Revalidate every hour (ISR)
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:
- Server Actions Guide
- Data Fetching Patterns
- Routing and Navigation
- Performance Optimization
Data Fetching Patterns
Advanced data fetching strategies for Next.js App Router.
Fetch API with Caching
Basic Fetch with Revalidation
// Revalidate every hour
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
}).then(res => res.json());
// Never cache (always fresh)
const user = await fetch('https://api.example.com/user', {
cache: 'no-store'
}).then(res => res.json());
// Cache indefinitely (until revalidated)
const settings = await fetch('https://api.example.com/settings', {
cache: 'force-cache'
}).then(res => res.json());With Tags for Revalidation
const posts = await fetch('https://api.example.com/posts', {
next: {
tags: ['posts'],
revalidate: 3600
}
}).then(res => res.json());
// Later, revalidate all requests tagged with 'posts'
import { revalidateTag } from 'next/cache';
revalidateTag('posts');Request Memoization
Next.js automatically memoizes fetch requests with the same URL and options:
export default async function Page() {
// These two fetch calls are automatically deduplicated
const post1 = await fetch('https://api.example.com/posts/1');
const post2 = await fetch('https://api.example.com/posts/1');
// Only one network request is made
}Manual Memoization
For non-fetch data fetching:
import { cache } from 'react';
const getPost = cache(async (id: string) => {
return await db.post.findUnique({
where: { id }
});
});
// Usage - automatically deduplicated
const post1 = await getPost('123');
const post2 = await getPost('123'); // Returns cached resultParallel Data Fetching
Using Promise.all
export default async function Dashboard() {
const [user, posts, notifications] = await Promise.all([
getUser(),
getPosts(),
getNotifications()
]);
return (
<div>
<UserInfo user={user} />
<PostsList posts={posts} />
<Notifications data={notifications} />
</div>
);
}With Error Handling
export default async function Dashboard() {
const results = await Promise.allSettled([
getUser(),
getPosts(),
getNotifications()
]);
const user = results[0].status === 'fulfilled' ? results[0].value : null;
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
const notifications = results[2].status === 'fulfilled' ? results[2].value : [];
return (
<div>
{user ? <UserInfo user={user} /> : <UserError />}
<PostsList posts={posts} />
<Notifications data={notifications} />
</div>
);
}Sequential Data Fetching
When data depends on previous results:
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const post = await getPost(id);
// Wait for post before fetching author
const author = await getAuthor(post.authorId);
// Wait for both before fetching related posts
const relatedPosts = await getRelatedPosts(post.tags);
return (
<article>
<Post data={post} author={author} />
<RelatedPosts posts={relatedPosts} />
</article>
);
}Streaming with Suspense
Basic Streaming
import { Suspense } from 'react';
export default function PostsPage() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<LoadingSkeleton />}>
<Posts />
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts(); // Slow fetch
return <PostsList posts={posts} />;
}Multiple Suspense Boundaries
export default function Dashboard() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserInfo />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<RecentPosts />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
</div>
);
}
async function UserInfo() {
const user = await getUser(); // Fast
return <UserCard user={user} />;
}
async function RecentPosts() {
const posts = await getPosts(); // Medium speed
return <PostsList posts={posts} />;
}
async function Stats() {
const stats = await calculateStats(); // Slow
return <StatsPanel data={stats} />;
}Database Queries
With Prisma
import { db } from '@/lib/db';
export default async function PostsPage() {
const posts = await db.post.findMany({
where: { published: true },
include: {
author: true,
comments: {
take: 5,
orderBy: { createdAt: 'desc' }
}
},
orderBy: { createdAt: 'desc' }
});
return <PostsList posts={posts} />;
}Optimized Queries
// Select only needed fields
const posts = await db.post.findMany({
select: {
id: true,
title: true,
excerpt: true,
author: {
select: {
name: true,
avatar: true
}
}
}
});
// Paginate results
const posts = await db.post.findMany({
take: 10,
skip: page * 10,
orderBy: { createdAt: 'desc' }
});GraphQL Queries
import { gql } from '@apollo/client';
import { getClient } from '@/lib/apollo-client';
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
author {
name
}
}
}
`;
export default async function PostsPage() {
const client = getClient();
const { data } = await client.query({
query: GET_POSTS
});
return <PostsList posts={data.posts} />;
}Error Handling
With try-catch
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
try {
const post = await getPost(id);
return <Post data={post} />;
} catch (error) {
if (error instanceof NotFoundError) {
notFound();
}
throw error; // Caught by error.tsx
}
}Using notFound()
import { notFound } from 'next/navigation';
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const post = await getPost(id);
if (!post) {
notFound(); // Renders not-found.tsx
}
return <Post data={post} />;
}Search Params
Reading Search Params
interface PageProps {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function PostsPage({ searchParams }: PageProps) {
const resolvedSearchParams = await searchParams;
const query = resolvedSearchParams.q as string | undefined;
const category = resolvedSearchParams.category as string | undefined;
const posts = await getPosts({
query,
category
});
return (
<div>
<SearchBar defaultQuery={query} />
<PostsList posts={posts} />
</div>
);
}Pagination with Search Params
export default async function PostsPage({ searchParams }: PageProps) {
const resolvedSearchParams = await searchParams;
const page = Number(resolvedSearchParams.page) || 1;
const limit = 10;
const [posts, total] = await Promise.all([
getPosts({ page, limit }),
getPostsCount()
]);
const totalPages = Math.ceil(total / limit);
return (
<div>
<PostsList posts={posts} />
<Pagination currentPage={page} totalPages={totalPages} />
</div>
);
}Static Generation
generateStaticParams
// Generate static pages at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
id: post.id
}));
}
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const post = await getPost(id);
return <Post data={post} />;
}With Dynamic Segments
// app/posts/[category]/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
category: post.category,
slug: post.slug
}));
}Incremental Static Regeneration (ISR)
export const revalidate = 3600; // Revalidate every hour
export default async function PostsPage() {
const posts = await getPosts();
return <PostsList posts={posts} />;
}On-Demand Revalidation
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const path = request.nextUrl.searchParams.get('path');
if (path) {
revalidatePath(path);
return Response.json({ revalidated: true, now: Date.now() });
}
return Response.json({
revalidated: false,
now: Date.now(),
message: 'Missing path to revalidate'
});
}Loading States
Skeleton Components
export function PostsSkeleton() {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2" />
<div className="h-4 bg-gray-200 rounded w-full mb-1" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
))}
</div>
);
}loading.tsx
// app/posts/loading.tsx
export default function Loading() {
return <PostsSkeleton />;
}Best Practices
1. Use Parallel Fetching: Fetch independent data in parallel with Promise.all 2. Implement Streaming: Use Suspense for progressive loading 3. Cache Strategically: Choose appropriate cache settings for each request 4. Handle Errors: Implement proper error boundaries and fallbacks 5. Optimize Queries: Select only needed fields from database 6. Use Static Generation: Pre-render pages when possible with generateStaticParams 7. Implement ISR: Use revalidate for automatic cache invalidation 8. Type Safety: Define proper types for all data fetching functions
Performance Optimization
Comprehensive guide to optimizing Next.js applications.
Image Optimization
Next.js Image Component
import Image from 'next/image';
export function PostCard({ post }: { post: Post }) {
return (
<div>
<Image
src={post.coverImage}
alt={post.title}
width={800}
height={400}
priority // Load eagerly (above fold)
/>
<h2>{post.title}</h2>
</div>
);
}Fill Layout
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/hero.jpg"
alt="Hero"
fill
style={{ objectFit: 'cover' }}
sizes="100vw"
/>
</div>Responsive Images
<Image
src="/post-cover.jpg"
alt="Post cover"
width={800}
height={400}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
/>Remote Images
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
],
},
};Code Splitting
Dynamic Imports
import dynamic from 'next/dynamic';
// Basic dynamic import
const Comments = dynamic(() => import('@/components/Comments'), {
loading: () => <p>Loading comments...</p>
});
export function PostPage({ post }: { post: Post }) {
return (
<article>
<Post data={post} />
<Comments postId={post.id} />
</article>
);
}Disable SSR
const Chart = dynamic(() => import('@/components/Chart'), {
ssr: false, // Only render on client
loading: () => <ChartSkeleton />
});Named Exports
const UserProfile = dynamic(
() => import('@/components/User').then(mod => mod.UserProfile),
{ loading: () => <Skeleton /> }
);Static Generation
generateStaticParams
// Pre-render all posts at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
id: post.id
}));
}
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return <Post data={post} />;
}Partial Pre-rendering
// Generate top 100 posts, rest on-demand
export const dynamicParams = true; // Allow dynamic params (default)
export async function generateStaticParams() {
const topPosts = await getTopPosts(100);
return topPosts.map((post) => ({
id: post.id
}));
}Incremental Static Regeneration
Time-Based Revalidation
// Revalidate every hour
export const revalidate = 3600;
export default async function PostsPage() {
const posts = await getPosts();
return <PostsList posts={posts} />;
}On-Demand Revalidation
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { path, tag } = await request.json();
if (path) {
revalidatePath(path);
}
if (tag) {
revalidateTag(tag);
}
return Response.json({ revalidated: true, now: Date.now() });
}Caching Strategies
Force Cache
// Cache indefinitely
fetch('https://api.example.com/settings', {
cache: 'force-cache'
});No Store
// Never cache
fetch('https://api.example.com/user', {
cache: 'no-store'
});Revalidate
// Cache for 1 hour
fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
});Tagged Caching
// Tag for selective revalidation
fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
});
// Later, revalidate
revalidateTag('posts');Streaming and Suspense
Progressive Loading
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
{/* Fast content loads immediately */}
<Header />
{/* Slow content streams in */}
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<RecentPosts />
</Suspense>
</div>
);
}Parallel Streaming
export default function Page() {
return (
<div>
<Suspense fallback={<Skeleton />}>
<SlowComponent1 />
</Suspense>
<Suspense fallback={<Skeleton />}>
<SlowComponent2 />
</Suspense>
{/* Both load in parallel */}
</div>
);
}Font Optimization
Google Fonts
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter'
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono'
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
);
}Local Fonts
import localFont from 'next/font/local';
const myFont = localFont({
src: './my-font.woff2',
display: 'swap',
variable: '--font-my-font'
});Route Segment Config
Dynamic Rendering
// Force dynamic rendering
export const dynamic = 'force-dynamic';
// Force static rendering
export const dynamic = 'force-static';
// Auto (default)
export const dynamic = 'auto';
// Error on dynamic
export const dynamic = 'error';Runtime
// Use Node.js runtime (default)
export const runtime = 'nodejs';
// Use Edge runtime
export const runtime = 'edge';Fetch Cache
// Default fetch cache behavior
export const fetchCache = 'auto';
// Force cache all fetches
export const fetchCache = 'force-cache';
// Never cache fetches
export const fetchCache = 'force-no-store';Lazy Loading Components
Client Components
'use client';
import dynamic from 'next/dynamic';
import { useState } from 'react';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
ssr: false
});
export function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>
Show Chart
</button>
{showChart && <HeavyChart />}
</div>
);
}Lazy Load External Libraries
'use client';
import { useState } from 'react';
export function MarkdownEditor() {
const [editor, setEditor] = useState<any>(null);
const loadEditor = async () => {
const { default: Editor } = await import('react-markdown-editor');
setEditor(<Editor />);
};
return (
<div>
{editor || <button onClick={loadEditor}>Load Editor</button>}
</div>
);
}Bundle Analysis
Setup
npm install @next/bundle-analyzer// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true'
});
module.exports = withBundleAnalyzer({
// Your Next.js config
});# Analyze bundle
ANALYZE=true npm run buildPerformance Monitoring
Web Vitals
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<Analytics />
</body>
</html>
);
}Custom Metrics
// app/web-vitals.ts
export function reportWebVitals(metric: any) {
console.log(metric);
// Send to analytics
if (metric.label === 'web-vital') {
// Track CLS, FID, FCP, LCP, TTFB
analytics.track('Web Vital', {
name: metric.name,
value: metric.value,
id: metric.id
});
}
}Database Optimization
Connection Pooling
// lib/db.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const db = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db;
}Query Optimization
// Select only needed fields
const posts = await db.post.findMany({
select: {
id: true,
title: true,
excerpt: true,
author: {
select: {
name: true,
avatar: true
}
}
},
take: 10
});
// Use indexes
await db.post.findMany({
where: {
published: true, // Should have index
categoryId: categoryId // Should have index
}
});Parallel Data Fetching
Component-Level
export default async function Dashboard() {
// Fetch in parallel
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
]);
return (
<div>
<UserProfile user={user} />
<PostsList posts={posts} />
<StatsPanel stats={stats} />
</div>
);
}With Suspense
export default function Dashboard() {
return (
<div>
{/* All load in parallel */}
<Suspense fallback={<Skeleton />}>
<UserInfo />
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentPosts />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Stats />
</Suspense>
</div>
);
}Metadata Optimization
Static Metadata
export const metadata = {
title: 'Posts',
description: 'Browse all posts',
openGraph: {
title: 'Posts',
description: 'Browse all posts',
images: ['/og-image.jpg']
}
};Dynamic Metadata
export async function generateMetadata({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage]
}
};
}Best Practices
1. Use Image Component: Always use next/image for automatic optimization 2. Implement Code Splitting: Use dynamic imports for heavy components 3. Static Generation: Pre-render pages when possible 4. Streaming: Use Suspense for progressive loading 5. Cache Strategically: Choose appropriate cache settings 6. Optimize Fonts: Use next/font for automatic font optimization 7. Parallel Fetching: Fetch independent data in parallel 8. Monitor Performance: Track Web Vitals and Core metrics 9. Analyze Bundles: Regularly check bundle size 10. Database Queries: Optimize queries and use connection pooling
Routing and Navigation
Comprehensive guide to Next.js App Router routing patterns.
File-Based Routing
Route Structure
app/
├── page.tsx # / (home)
├── about/
│ └── page.tsx # /about
├── posts/
│ ├── page.tsx # /posts
│ ├── [id]/
│ │ └── page.tsx # /posts/:id
│ └── [category]/
│ └── [slug]/
│ └── page.tsx # /posts/:category/:slug
└── (marketing)/
├── pricing/
│ └── page.tsx # /pricing (grouped without path segment)
└── features/
└── page.tsx # /featuresSpecial Files
page.tsx
Defines the UI for a route:
export default function PostsPage() {
return <div>Posts page</div>;
}layout.tsx
Shared UI that wraps pages:
export default function PostsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<PostsHeader />
{children}
<PostsFooter />
</div>
);
}template.tsx
Similar to layout but creates new instance on navigation:
export default function PostsTemplate({
children,
}: {
children: React.ReactNode;
}) {
return <div className="posts-wrapper">{children}</div>;
}loading.tsx
Loading UI for route segment:
export default function Loading() {
return <Skeleton />;
}error.tsx
Error boundary for route segment:
'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>
);
}not-found.tsx
404 UI:
export default function NotFound() {
return <div>404 - Page not found</div>;
}Dynamic Routes
Single Dynamic Segment
// app/posts/[id]/page.tsx
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function PostPage({ params }: PageProps) {
const { id } = await params;
return <div>Post ID: {id}</div>;
}Multiple Dynamic Segments
// app/posts/[category]/[slug]/page.tsx
interface PageProps {
params: Promise<{
category: string;
slug: string;
}>;
}
export default async function PostPage({ params }: PageProps) {
const { category, slug } = await params;
return (
<div>
Category: {category}, Slug: {slug}
</div>
);
}Catch-All Segments
// app/docs/[...slug]/page.tsx
interface PageProps {
params: Promise<{ slug: string[] }>;
}
export default async function DocsPage({ params }: PageProps) {
const { slug } = await params;
// /docs/a/b/c -> slug = ['a', 'b', 'c']
return <div>Path: {slug.join('/')}</div>;
}Optional Catch-All Segments
// app/shop/[[...categories]]/page.tsx
interface PageProps {
params: Promise<{ categories?: string[] }>;
}
export default async function ShopPage({ params }: PageProps) {
const { categories } = await params;
// /shop -> categories = undefined
// /shop/clothes -> categories = ['clothes']
// /shop/clothes/shirts -> categories = ['clothes', 'shirts']
return <div>Categories: {categories?.join('/') || 'All'}</div>;
}Route Groups
Group routes without affecting URL structure:
app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
├── layout.tsx # Shop layout
├── products/
│ └── page.tsx # /products
└── cart/
└── page.tsx # /cart// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<MarketingNav />
{children}
</div>
);
}Parallel Routes
Render multiple pages in the same layout:
app/
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
├── layout.tsx
└── page.tsx// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
<div>{children}</div>
<div>{analytics}</div>
<div>{team}</div>
</div>
);
}Intercepting Routes
Intercept routes and display in modal:
app/
├── feed/
│ └── page.tsx
├── @modal/
│ └── (..)photo/
│ └── [id]/
│ └── page.tsx
└── photo/
└── [id]/
└── page.tsx// app/@modal/(..)photo/[id]/page.tsx
export default async function PhotoModal({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
return <Modal><Photo id={id} /></Modal>;
}
// app/photo/[id]/page.tsx (for direct navigation)
export default async function PhotoPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
return <Photo id={id} />;
}Navigation
Link Component
import Link from 'next/link';
// Basic link
<Link href="/posts">Posts</Link>
// Dynamic route
<Link href={`/posts/${post.id}`}>View Post</Link>
// With search params
<Link href={{ pathname: '/posts', query: { category: 'tech' } }}>
Tech Posts
</Link>
// Prefetching control
<Link href="/posts" prefetch={false}>Posts</Link>Programmatic Navigation
'use client';
import { useRouter } from 'next/navigation';
export function LoginButton() {
const router = useRouter();
return (
<button onClick={() => router.push('/login')}>
Login
</button>
);
}Router Methods
'use client';
import { useRouter } from 'next/navigation';
export function NavigationExample() {
const router = useRouter();
return (
<div>
<button onClick={() => router.push('/posts')}>Push</button>
<button onClick={() => router.replace('/posts')}>Replace</button>
<button onClick={() => router.back()}>Back</button>
<button onClick={() => router.forward()}>Forward</button>
<button onClick={() => router.refresh()}>Refresh</button>
<button onClick={() => router.prefetch('/posts')}>Prefetch</button>
</div>
);
}Pathname and Params
usePathname
'use client';
import { usePathname } from 'next/navigation';
export function Navigation() {
const pathname = usePathname(); // e.g., /posts/123
return (
<nav>
<Link
href="/posts"
className={pathname === '/posts' ? 'active' : ''}
>
Posts
</Link>
</nav>
);
}useParams
'use client';
import { useParams } from 'next/navigation';
export function PostActions() {
const params = useParams(); // { id: '123' }
return <div>Post ID: {params.id}</div>;
}useSearchParams
'use client';
import { useSearchParams } from 'next/navigation';
export function SearchResults() {
const searchParams = useSearchParams();
const query = searchParams.get('q'); // ?q=hello
return <div>Searching for: {query}</div>;
}Middleware
Basic Middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Clone and modify headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-pathname', request.nextUrl.pathname);
return NextResponse.next({
request: {
headers: requestHeaders
}
});
}
export const config = {
matcher: '/posts/:path*'
};Authentication Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*']
};Redirects
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/old-posts') {
return NextResponse.redirect(new URL('/posts', request.url));
}
return NextResponse.next();
}Rewrite
export function middleware(request: NextRequest) {
// Rewrite /blog/* to /posts/*
if (request.nextUrl.pathname.startsWith('/blog')) {
return NextResponse.rewrite(
new URL(request.nextUrl.pathname.replace('/blog', '/posts'), request.url)
);
}
return NextResponse.next();
}Redirects
In Server Components
import { redirect } from 'next/navigation';
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const post = await getPost(id);
if (!post.published) {
redirect('/posts');
}
return <Post data={post} />;
}In Server Actions
'use server';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const post = await db.post.create({
data: { title: formData.get('title') as string }
});
redirect(`/posts/${post.id}`);
}Permanent Redirects
import { permanentRedirect } from 'next/navigation';
export default async function OldPostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
permanentRedirect(`/posts/${id}`);
}Route Handlers (API Routes)
Basic Handler
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const posts = await getPosts();
return NextResponse.json({ posts });
}
export async function POST(request: Request) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json({ post }, { status: 201 });
}Dynamic Route Handler
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id);
if (!post) {
return NextResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
}
return NextResponse.json({ post });
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const post = await updatePost(params.id, body);
return NextResponse.json({ post });
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await deletePost(params.id);
return new NextResponse(null, { status: 204 });
}Headers and Cookies
import { cookies, headers } from 'next/headers';
export async function GET() {
// Read headers
const headersList = headers();
const userAgent = headersList.get('user-agent');
// Read cookies
const cookieStore = cookies();
const token = cookieStore.get('token');
return NextResponse.json({ userAgent, token });
}
export async function POST(request: Request) {
const response = NextResponse.json({ success: true });
// Set cookie
response.cookies.set('token', 'abc123', {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 // 1 day
});
return response;
}Best Practices
1. Use File-Based Routing: Leverage Next.js conventions for organization 2. Group Related Routes: Use route groups to share layouts 3. Implement Error Boundaries: Add error.tsx for graceful error handling 4. Use Loading States: Create loading.tsx for better UX 5. Type Route Params: Always define types for params and searchParams 6. Prefetch Links: Use Link component for automatic prefetching 7. Middleware for Auth: Implement authentication checks in middleware 8. Handle 404s: Create not-found.tsx for custom 404 pages
Server Actions Guide
Comprehensive patterns for Next.js Server Actions.
What are Server Actions?
Server Actions are asynchronous functions that run on the server. They can be called from Client or Server Components.
'use server';
export async function createPost(formData: FormData) {
// Runs on server
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
}Form Actions
Basic Form Action
// app/actions/posts.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
const postSchema = z.object({
title: z.string().min(1),
content: z.string().min(10)
});
export async function createPost(formData: FormData) {
const validatedFields = postSchema.safeParse({
title: formData.get('title'),
content: formData.get('content')
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Validation failed'
};
}
const { title, content } = validatedFields.data;
try {
await db.post.create({
data: { title, content, published: false }
});
} catch (error) {
return {
message: 'Database Error: Failed to create post'
};
}
revalidatePath('/posts');
redirect('/posts');
}Using in Forms
// app/posts/new/page.tsx
import { createPost } from '@/app/actions/posts';
export default function NewPostPage() {
return (
<form action={createPost}>
<div>
<label htmlFor="title">Title</label>
<input id="title" name="title" type="text" required />
</div>
<div>
<label htmlFor="content">Content</label>
<textarea id="content" name="content" required />
</div>
<button type="submit">Create Post</button>
</form>
);
}Client Component Integration
With useFormState
// app/actions/posts.ts
'use server';
export type FormState = {
errors?: {
title?: string[];
content?: string[];
};
message?: string;
};
export async function createPost(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const validatedFields = postSchema.safeParse({
title: formData.get('title'),
content: formData.get('content')
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing fields'
};
}
// Create post...
return { message: 'Post created successfully' };
}// components/CreatePostForm.tsx
'use client';
import { useFormState } from 'react-dom';
import { createPost } from '@/app/actions/posts';
const initialState = { message: '', errors: {} };
export function CreatePostForm() {
const [state, formAction] = useFormState(createPost, initialState);
return (
<form action={formAction}>
<div>
<label htmlFor="title">Title</label>
<input id="title" name="title" />
{state.errors?.title && (
<p className="error">{state.errors.title[0]}</p>
)}
</div>
<div>
<label htmlFor="content">Content</label>
<textarea id="content" name="content" />
{state.errors?.content && (
<p className="error">{state.errors.content[0]}</p>
)}
</div>
{state.message && <p>{state.message}</p>}
<button type="submit">Create Post</button>
</form>
);
}With useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton({ label }: { label: string }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} aria-disabled={pending}>
{pending ? 'Submitting...' : label}
</button>
);
}
// Usage in form
export function CreatePostForm() {
return (
<form action={createPost}>
{/* form fields */}
<SubmitButton label="Create Post" />
</form>
);
}Non-Form Actions
Programmatic Invocation
// app/actions/posts.ts
'use server';
export async function deletePost(postId: string) {
await db.post.delete({
where: { id: postId }
});
revalidatePath('/posts');
}
export async function publishPost(postId: string) {
await db.post.update({
where: { id: postId },
data: { published: true, publishedAt: new Date() }
});
revalidatePath('/posts');
revalidatePath(`/posts/${postId}`);
}// components/PostActions.tsx
'use client';
import { deletePost, publishPost } from '@/app/actions/posts';
import { useRouter } from 'next/navigation';
export function PostActions({ postId }: { postId: string }) {
const router = useRouter();
const handleDelete = async () => {
if (confirm('Are you sure?')) {
await deletePost(postId);
router.push('/posts');
}
};
const handlePublish = async () => {
await publishPost(postId);
};
return (
<div>
<button onClick={handlePublish}>Publish</button>
<button onClick={handleDelete}>Delete</button>
</div>
);
}Authentication and Authorization
Checking User Session
'use server';
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const session = await auth();
if (!session?.user) {
redirect('/login');
}
const post = await db.post.create({
data: {
title: formData.get('title') as string,
content: formData.get('content') as string,
authorId: session.user.id
}
});
revalidatePath('/posts');
return { success: true, postId: post.id };
}Role-Based Actions
'use server';
export async function deletePost(postId: string) {
const session = await auth();
if (!session?.user) {
throw new Error('Not authenticated');
}
const post = await db.post.findUnique({
where: { id: postId }
});
if (post.authorId !== session.user.id && session.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
await db.post.delete({
where: { id: postId }
});
revalidatePath('/posts');
}Error Handling
Try-Catch Pattern
'use server';
export async function createPost(formData: FormData) {
try {
const post = await db.post.create({
data: {
title: formData.get('title') as string,
content: formData.get('content') as string
}
});
revalidatePath('/posts');
return { success: true, post };
} catch (error) {
console.error('Failed to create post:', error);
return {
success: false,
error: 'Failed to create post. Please try again.'
};
}
}Custom Error Types
class PostNotFoundError extends Error {
constructor(postId: string) {
super(`Post ${postId} not found`);
this.name = 'PostNotFoundError';
}
}
class UnauthorizedError extends Error {
constructor() {
super('You are not authorized to perform this action');
this.name = 'UnauthorizedError';
}
}
export async function updatePost(postId: string, data: PostUpdate) {
const session = await auth();
if (!session) throw new UnauthorizedError();
const post = await db.post.findUnique({ where: { id: postId } });
if (!post) throw new PostNotFoundError(postId);
if (post.authorId !== session.user.id) {
throw new UnauthorizedError();
}
return await db.post.update({
where: { id: postId },
data
});
}Optimistic Updates
Client-Side Optimistic Update
'use client';
import { useState, useTransition } from 'react';
import { likePost } from '@/app/actions/posts';
export function LikeButton({ postId, initialLikes }: Props) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
const handleLike = () => {
// Optimistic update
setLikes(prev => prev + 1);
startTransition(async () => {
try {
const result = await likePost(postId);
setLikes(result.likes); // Update with server value
} catch (error) {
// Rollback on error
setLikes(prev => prev - 1);
}
});
};
return (
<button onClick={handleLike} disabled={isPending}>
❤️ {likes}
</button>
);
}File Uploads
Handling File Uploads
'use server';
import { writeFile } from 'fs/promises';
import { join } from 'path';
export async function uploadImage(formData: FormData) {
const file = formData.get('image') as File;
if (!file) {
return { error: 'No file uploaded' };
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Save to public directory
const path = join(process.cwd(), 'public/uploads', file.name);
await writeFile(path, buffer);
return { success: true, path: `/uploads/${file.name}` };
}With Image Upload
'use server';
import { put } from '@vercel/blob';
export async function createPostWithImage(formData: FormData) {
const file = formData.get('coverImage') as File;
const title = formData.get('title') as string;
const content = formData.get('content') as string;
let imageUrl = '';
if (file && file.size > 0) {
const blob = await put(file.name, file, {
access: 'public'
});
imageUrl = blob.url;
}
const post = await db.post.create({
data: {
title,
content,
coverImage: imageUrl
}
});
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
}Revalidation Strategies
Path Revalidation
import { revalidatePath } from 'next/cache';
// Revalidate specific page
revalidatePath('/posts');
// Revalidate dynamic route
revalidatePath(`/posts/${postId}`);
// Revalidate layout
revalidatePath('/posts', 'layout');Tag Revalidation
import { revalidateTag } from 'next/cache';
// In fetch
fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
});
// Revalidate
revalidateTag('posts');Best Practices
1. Always use 'use server': Mark Server Actions with the directive 2. Validate Input: Use Zod or similar for validation 3. Handle Errors: Always wrap in try-catch 4. Type Safety: Define proper return types 5. Revalidate: Call revalidatePath/revalidateTag after mutations 6. Security: Check authentication and authorization 7. Progressive Enhancement: Forms work without JavaScript 8. Optimistic Updates: Use for better UX on user actions
{
"nextjs": {
"type": "domain",
"enforcement": "suggest",
"priority": "high",
"promptTriggers": {
"keywords": [
"next.js",
"nextjs",
"app router",
"app directory",
"server component",
"client component",
"server action",
"generateMetadata",
"generateStaticParams",
"next/navigation",
"next/link",
"use client"
],
"intentPatterns": [
"create.*(page|route|layout|component)",
"add.*(page|route|layout|component)",
"build.*component",
"make.*component",
"add.*route.*handler",
"create.*api.*route",
"add.*server.*action",
"create.*server.*component",
"create.*client.*component",
"add.*metadata",
"create.*loading.*state",
"add.*error.*boundary",
"implement.*dynamic.*route"
]
},
"fileTriggers": {
"pathPatterns": [
"**/app/**/*.tsx",
"**/app/**/*.ts",
"**/components/**/*.tsx",
"**/next.config.js",
"**/next.config.mjs"
]
}
}
}
Related skills
FAQ
Which Next.js version does this cover?
Next.js 15+ using the App Router, Server Components, and modern data fetching.
When should I use a Client Component?
Use Client Components for event listeners, state and lifecycle hooks, and browser-only APIs; otherwise default to Server Components.