nextjs
SKILL.md
Next.js - App Router & Server Components
Modern patterns for Next.js 15 with App Router, Server Components, and streaming
When to Use This Skill
Use this skill when:
- Building Next.js 15+ applications with App Router
- Implementing Server Components and Client Components
- Setting up API routes (Route Handlers)
- Configuring middleware for auth, redirects, or rewrites
- Optimizing performance with streaming and suspense
- Implementing data fetching patterns
Don't use this skill when:
- Using Pages Router (legacy pattern)
- Building pure React SPA (no Next.js)
- Working with other frameworks (Remix, Astro)
Critical Patterns
Pattern 1: Server Components vs Client Components
When: Deciding where to render components
// ✅ GOOD: Server Component (default) - no "use client"
// app/products/page.tsx
async function ProductsPage() {
const products = await db.product.findMany(); // Direct DB access
return (
<div>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
// ✅ GOOD: Client Component - only when needed
// components/add-to-cart.tsx
"use client";
import { useState } from 'react';
export function AddToCart({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
await addToCart(productId);
setLoading(false);
}
return <button onClick={handleClick} disabled={loading}>Add to Cart</button>;
}
// ❌ BAD: Adding "use client" unnecessarily
"use client"; // Don't add this if component doesn't use hooks/events
export function ProductCard({ product }) {
return <div>{product.name}</div>;
}
Rule of thumb: Keep components Server by default. Only add "use client" when you need:
useState,useEffect, or other hooks- Event handlers (
onClick,onChange) - Browser-only APIs (
window,localStorage)
Pattern 2: Route Handlers (API Routes)
When: Building API endpoints in App Router
// ✅ GOOD: app/api/products/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category');
const products = await db.product.findMany({
where: category ? { category } : undefined
});
return NextResponse.json(products);
}
export async function POST(request: Request) {
const body = await request.json();
// Validate input
const validated = productSchema.safeParse(body);
if (!validated.success) {
return NextResponse.json(
{ error: validated.error.flatten() },
{ status: 400 }
);
}
const product = await db.product.create({ data: validated.data });
return NextResponse.json(product, { status: 201 });
}
// ❌ BAD: Using pages/api pattern in app directory
// pages/api/products.ts - This is the old pattern!
export default function handler(req, res) {
res.json({ products: [] });
}
Pattern 3: Streaming with Suspense
When: Loading data progressively for better UX
// ✅ GOOD: Stream slow data with Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Fast content renders immediately */}
<QuickStats />
{/* Slow content streams in with loading state */}
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<RecentOrdersSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
// Server Component with slow data fetch
async function RevenueChart() {
const data = await fetchRevenueData(); // Slow API call
return <Chart data={data} />;
}
// ❌ BAD: Blocking entire page on slow data
async function DashboardPage() {
const [stats, revenue, orders] = await Promise.all([
fetchStats(),
fetchRevenueData(), // Slow!
fetchRecentOrders() // Also slow!
]);
// User waits for ALL data before seeing anything
return <div>...</div>;
}
Pattern 4: Middleware for Auth & Redirects
When: Protecting routes or modifying requests
// ✅ GOOD: middleware.ts (root of project)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
const isAuthPage = request.nextUrl.pathname.startsWith('/login');
const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');
// Redirect to login if not authenticated
if (isProtectedRoute && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Redirect to dashboard if already logged in
if (isAuthPage && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};
// ❌ BAD: Checking auth in every page component
async function DashboardPage() {
const session = await getSession();
if (!session) redirect('/login'); // Too late, page already loaded
// ...
}
Pattern 5: Server Actions
When: Mutating data from Server Components
// ✅ GOOD: Server Action in separate file
// app/actions/products.ts
"use server";
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1),
price: z.number().positive(),
});
export async function createProduct(formData: FormData) {
const validated = schema.safeParse({
name: formData.get('name'),
price: Number(formData.get('price')),
});
if (!validated.success) {
return { error: validated.error.flatten() };
}
await db.product.create({ data: validated.data });
revalidatePath('/products');
return { success: true };
}
// Usage in Server Component
// app/products/new/page.tsx
import { createProduct } from '@/app/actions/products';
export default function NewProductPage() {
return (
<form action={createProduct}>
<input name="name" required />
<input name="price" type="number" required />
<button type="submit">Create</button>
</form>
);
}
// ❌ BAD: API route for simple mutations
// Don't create /api/products POST just to call from form
Code Examples
Example 1: Dynamic Route with Params
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
interface Props {
params: Promise<{ id: string }>;
}
export default async function ProductPage({ params }: Props) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) {
notFound(); // Renders not-found.tsx
}
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
</div>
);
}
// Generate static params for SSG
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { id: true } });
return products.map(p => ({ id: p.id }));
}
Example 2: Parallel Routes with Loading States
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-2 gap-4">
<div>{children}</div>
<div>{analytics}</div>
<div className="col-span-2">{team}</div>
</div>
);
}
// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
const data = await fetchAnalytics();
return <AnalyticsChart data={data} />;
}
// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return <Skeleton className="h-64" />;
}
Example 3: Error Handling
// app/products/error.tsx
"use client";
export default function ProductsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="p-4 border border-red-500 rounded">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/products/not-found.tsx
export default function ProductNotFound() {
return (
<div className="p-4">
<h2>Product Not Found</h2>
<p>The product you're looking for doesn't exist.</p>
</div>
);
}
Anti-Patterns
Don't: Fetch Data in Client Components
// ❌ BAD: Fetching in Client Component
"use client";
export function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return <div>{/* render products */}</div>;
}
// ✅ GOOD: Fetch in Server Component, pass to Client
async function ProductsPage() {
const products = await db.product.findMany();
return <ProductList products={products} />;
}
Don't: Pass Functions to Client Components
// ❌ BAD: Passing server function directly
async function Page() {
async function handleSubmit() {
"use server";
await db.product.create({ data });
}
return <ClientForm onSubmit={handleSubmit} />; // Won't work!
}
// ✅ GOOD: Use Server Actions properly
import { createProduct } from '@/app/actions';
function Page() {
return <form action={createProduct}>...</form>;
}
Don't: Over-use "use client"
// ❌ BAD: Making everything a Client Component
"use client"; // at the top of every file
// ✅ GOOD: Push "use client" to leaf components
// Keep pages and layouts as Server Components
// Only add "use client" to interactive parts
Quick Reference
| Task | Pattern | Example |
|---|---|---|
| Protected route | Middleware | middleware.ts with matcher |
| API endpoint | Route Handler | app/api/x/route.ts |
| Form submission | Server Action | "use server" function |
| Loading state | Suspense | <Suspense fallback={...}> |
| 404 page | not-found.tsx | notFound() function |
| Error handling | error.tsx | Client Component with reset |
| Dynamic route | [param] folder | app/posts/[id]/page.tsx |
| Parallel data | Parallel routes | @slot folders |
Resources
Official Documentation:
Related Skills:
- react: React patterns and hooks
- typescript: Type safety patterns
- api-design: API route design
References:
Keywords
nextjs, next.js, app-router, server-components, rsc, streaming, suspense, middleware, route-handlers, server-actions, ssr, ssg, isr
Weekly Installs
2
Repository
dsantiagomj/dsm…-toolkitFirst Seen
Feb 25, 2026
Security Audits
Installed on
trae-cn2
codebuddy2
github-copilot2
codex2
kiro-cli2
kimi-cli2