
Nextjs App Router
- 1.6k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
nextjs-app-router is an agent skill that provides patterns and code examples for building next.js 16+ applications with app router architecture. use when creating projects with app router, implementing server components
About
nextjs-app-router is an agent skill from giuseppe-trisciuoglio/developer-kit that provides patterns and code examples for building next.js 16+ applications with app router architecture. use when creating projects with app router, implementing server components and client components. # Next.js App Router (Next.js 16+) Build modern React applications using Next.js 16+ with App Router architecture. ## Overview This skill provides patterns for Server Components (default) and Client Components ("use client"), Server Actions for mutations and form handling, Route Handlers for API endpoints, explicit caching with "use cache" direc Developers invoke nextjs-app-router during build/backend work for backend & apis tasks. The skill documents triggers, prerequisites, and step-by-step workflows grounded in SKILL.md. Compatible with Claude Code, Cursor, and Codex agent runtimes that load marketplace skills.
- Next.js App Router (Next.js 16+)
- Build modern React applications using Next.js 16+ with App Router architecture.
- Activate when user requests involve:
- "Create a Next.js 16 project", "Set up App Router"
- "Server Component", "Client Component", "use client"
Nextjs App Router by the numbers
- 1,620 all-time installs (skills.sh)
- +67 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #289 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
nextjs-app-router capabilities & compatibility
- Capabilities
- next.js app router (next.js 16+) · build modern react applications using next.js 16 · activate when user requests involve: · "create a next.js 16 project", "set up app route · "server component", "client component", "use cli
- Use cases
- orchestration
What nextjs-app-router says it does
Build modern React applications using Next.js 16+ with App Router architecture.
- "Create a Next.js 16 project", "Set up App Router"
- "Server Component", "Client Component", "use client"
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-app-routerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
What it does
Provides patterns and code examples for building Next.js 16+ applications with App Router architecture. Use when creating projects with App Router, implementing Server Components and Client Components
Who is it for?
Developers working on backend & apis during build tasks.
Skip if: Tasks outside Backend & APIs scope described in SKILL.md.
When should I use this skill?
Provides patterns and code examples for building Next.js 16+ applications with App Router architecture. Use when creating projects with App Router, implementing Server Components and Client Components
What you get
Completed backend & apis workflow aligned with SKILL.md steps.
- server component pages
- client component modules
- correct rendering boundaries
Files
Next.js App Router (Next.js 16+)
Build modern React applications using Next.js 16+ with App Router architecture.
Overview
This skill provides patterns for Server Components (default) and Client Components ("use client"), Server Actions for mutations and form handling, Route Handlers for API endpoints, explicit caching with "use cache" directive, parallel and intercepting routes, and Next.js 16 async APIs and proxy.ts.
When to Use
Activate when user requests involve:
- "Create a Next.js 16 project", "Set up App Router"
- "Server Component", "Client Component", "use client"
- "Server Action", "form submission", "mutation"
- "Route Handler", "API endpoint", "route.ts"
- "use cache", "cacheLife", "cacheTag", "revalidation"
- "parallel routes", "
@slot", "intercepting routes" - "proxy.ts", "migrate from middleware.ts"
- "layout.tsx", "page.tsx", "loading.tsx", "error.tsx", "not-found.tsx"
- "generateMetadata", "next/image", "next/font"
Quick Reference
| File | Purpose | Directive | Purpose |
|---|---|---|---|
page.tsx | Route page | "use server" | Server Action function |
layout.tsx | Shared layout | "use client" | Client Component boundary |
loading.tsx | Suspense loading | "use cache" | Explicit caching (Next.js 16) |
error.tsx | Error boundary | ||
not-found.tsx | 404 page | ||
route.ts | API Route Handler | ||
proxy.ts | Routing boundary |
Instructions
Create New Project
npx create-next-app@latest my-app --typescript --tailwind --app --turbopackServer Component
Server Components are the default in App Router. They run on the server and can use async/await.
// app/users/page.tsx
async function getUsers() {
const apiUrl = process.env.API_URL;
const res = await fetch(`${apiUrl}/users`);
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return <main>{users.map(user => <UserCard key={user.id} user={user} />)}</main>;
}Client Component
Add "use client" when using hooks, browser APIs, or event handlers.
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}Server Action
Define actions in separate files with "use server" directive.
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
await db.user.create({ data: { name, email } });
revalidatePath("/users");
}Use with forms in Client Components:
"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
export default function UserForm() {
const [state, formAction, pending] = useActionState(createUser, {});
return (
<form action={formAction}>
<input name="name" />
<input name="email" type="email" />
<button type="submit" disabled={pending}>{pending ? "Creating..." : "Create"}</button>
</form>
);
}See references/server-actions.md for Zod validation, optimistic updates, and advanced patterns.
Configure Caching
Use "use cache" directive for explicit caching (Next.js 16+).
"use cache";
import { cacheLife, cacheTag } from "next/cache";
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
cacheTag(`product-${id}`);
cacheLife("hours");
const product = await fetchProduct(id);
return <ProductDetail product={product} />;
}See references/caching-strategies.md for cache profiles, on-demand revalidation, and advanced patterns.
Route Handler
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
return NextResponse.json(await db.user.findMany());
}
export async function POST(request: NextRequest) {
const body = await request.json();
return NextResponse.json(await db.user.create({ data: body }), { status: 201 });
}Dynamic segments use [param]:
// app/api/users/[id]/route.ts
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await db.user.findUnique({ where: { id } });
if (!user) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(user);
}Next.js 16 Async APIs
All Next.js APIs are async in version 16.
import { cookies, headers } from "next/headers";
export default async function Page() {
const cookieStore = await cookies();
const headersList = await headers();
const session = cookieStore.get("session")?.value;
const userAgent = headersList.get("user-agent");
return <div>...</div>;
}Params and searchParams are also Promise-based:
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ sort?: string }>;
}) {
const { slug } = await params;
const { sort } = await searchParams;
// ...
}See references/nextjs16-migration.md for migration guide and proxy.ts configuration.
Parallel Routes
Use @folder convention for parallel route slots.
// app/dashboard/layout.tsx
export default function DashboardLayout({ children, team, analytics }: Record<string, React.ReactNode>) {
return (
<div>
{children}
<div className="grid grid-cols-2">{team}{analytics}</div>
</div>
);
}// app/dashboard/@team/page.tsx
export default function TeamPage() { return <div>Team Section</div>; }
// app/dashboard/@analytics/page.tsx
export default function AnalyticsPage() { return <div>Analytics Section</div>; }See references/routing-patterns.md for intercepting routes, route groups, and dynamic routes.
Best Practices
Server vs Client Decision:
- Start with Server Component (default)
- Use Client Component only for: React hooks (useState, useEffect), browser APIs (window, document), event handlers (onClick, onSubmit), or client-only libraries
Data Fetching:
- Fetch in Server Components when possible
- Use React's
cache()for deduplication - Parallelize independent fetches
- Add Suspense boundaries with
loading.tsx
Performance Checklist:
- Use
loading.tsxfor Suspense boundaries - Use
next/imagefor optimized images - Use
next/fontfor font optimization - Add
error.tsxandnot-found.tsxfor error handling
Examples
Example 1: Blog Post Form with Server Action
Input: Create a form to submit blog posts with Zod validation
Output:
// app/blog/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const schema = z.object({ title: z.string().min(5), content: z.string().min(10) });
export async function createPost(formData: FormData) {
const parsed = schema.safeParse({ title: formData.get("title"), content: formData.get("content") });
if (!parsed.success) return { errors: parsed.error.flatten().fieldErrors };
await db.post.create({ data: parsed.data });
revalidatePath("/blog");
return { success: true };
}// app/blog/new/page.tsx
"use client";
import { useActionState } from "react";
import { createPost } from "../actions";
export default function NewPostPage() {
const [state, formAction, pending] = useActionState(createPost, {});
return (
<form action={formAction}>
<input name="title" placeholder="Title" />
{state.errors?.title && <span>{state.errors.title[0]}</span>}
<textarea name="content" placeholder="Content" />
{state.errors?.content && <span>{state.errors.content[0]}</span>}
<button type="submit" disabled={pending}>{pending ? "Publishing..." : "Publish"}</button>
</form>
);
}Example 2: Cached Product Page
Input: Create a cached product page with on-demand revalidation
Output:
// app/products/[id]/page.tsx
"use cache";
import { cacheLife, cacheTag } from "next/cache";
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
cacheTag(`product-${id}`, "products");
cacheLife("hours");
const product = await db.product.findUnique({ where: { id } });
if (!product) notFound();
return <article><h1>{product.name}</h1><p>{product.description}</p></article>;
}// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { tag } = await request.json();
revalidateTag(tag);
return NextResponse.json({ revalidated: true });
}Example 3: Dashboard with Parallel Routes
Input: Create a dashboard with sidebar and stats areas
Output:
// app/dashboard/layout.tsx
export default function DashboardLayout({ children, sidebar, stats }: Record<string, React.ReactNode>) {
return (
<div className="flex">
<aside className="w-64">{sidebar}</aside>
<main className="flex-1"><div className="grid grid-cols-3">{stats}</div>{children}</main>
</div>
);
}// app/dashboard/@sidebar/page.tsx
export default function Sidebar() { return <nav>{/* Navigation links */}</nav>; }
// app/dashboard/@stats/page.tsx
export default async function Stats() {
const stats = await fetchStats();
return <><div>Users: {stats.users}</div><div>Orders: {stats.orders}</div></>;
}Constraints and Warnings
Constraints:
- Server Components cannot use browser APIs or React hooks
- Client Components cannot be async (no direct data fetching)
cookies(),headers(),draftMode()are async in Next.js 16paramsandsearchParamsare Promise-based in Next.js 16- Server Actions must be defined with
"use server"directive
Warnings:
- Using
awaitin a Client Component causes a build error - Accessing
windowordocumentin Server Components throws an error - Forgetting to
awaitcookies() or headers() in Next.js 16 returns a Promise instead of the value - Server Actions without proper validation can expose the database to unauthorized access
- External Data Fetching: Server Components that
fetch()third-party URLs process untrusted content — always validate, sanitize, and type-check responses; use environment variables for API URLs rather than hardcoding them
References
- [references/app-router-fundamentals.md](references/app-router-fundamentals.md) — Server/Client Components, file conventions, navigation
- [references/routing-patterns.md](references/routing-patterns.md) — Parallel routes, intercepting routes, route groups
- [references/caching-strategies.md](references/caching-strategies.md) — "use cache", cacheLife, cacheTag, revalidation
- [references/server-actions.md](references/server-actions.md) — Server Actions, useActionState, validation, optimistic updates
- [references/nextjs16-migration.md](references/nextjs16-migration.md) — Async APIs, proxy.ts, Turbopack, config
- [references/data-fetching.md](references/data-fetching.md) — Data patterns, Suspense, streaming
- [references/metadata-api.md](references/metadata-api.md) — generateMetadata, OpenGraph, sitemap
Next.js App Router Fundamentals
Server Components vs Client Components
Server Components (Default)
I Server Components sono il default in App Router. Eseguono sul server e possono:
- Accedere direttamente a database, file system, API esterne
- Renderizzare dati sensibili senza esporli al client
- Ridurre il bundle JavaScript inviato al client
// app/page.tsx - Server Component di default
async function getData() {
const res = await fetch(`${process.env.API_URL}/data`);
return res.json();
}
export default async function Page() {
const data = await getData();
return <main>{/* render data */}</main>;
}Client Components
Usare "use client" per componenti che necessitano di:
- React hooks (useState, useEffect, useContext)
- Browser APIs (window, document, localStorage)
- Event handlers (onClick, onSubmit)
- Third-party librerie client-only
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}Pattern Ibrido (Server + Client)
// app/page.tsx - Server Component
import { ProductCard } from "./product-card";
async function getProducts() {
return db.product.findMany();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}// app/product-card.tsx - Client Component
"use client";
import { useState } from "react";
export function ProductCard({ product }: { product: Product }) {
const [isAdded, setIsAdded] = useState(false);
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => setIsAdded(true)}>
{isAdded ? "Added!" : "Add to Cart"}
</button>
</div>
);
}React Compiler (Next.js 16+)
Next.js 16 include React Compiler per memoizzazione automatica.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default nextConfig;Con React Compiler attivo, non serve più manualmente:
React.memo()(in molti casi)useMemo()per computazioni sempliciuseCallback()per event handlers semplici
File Conventions Speciali
loading.tsx
Mostra UI durante il caricamento di dati.
// app/blog/loading.tsx
export default function Loading() {
return <p>Loading posts...</p>;
}error.tsx
Gestisce errori nel segmento con Error Boundary.
// app/blog/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>
);
}not-found.tsx
Pagina 404 per segmento.
// app/blog/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
</div>
);
}// Per triggerare programmaticamente
import { notFound } from "next/navigation";
export default async function BlogPost({ params }: PageProps) {
const post = await fetchPost((await params).slug);
if (!post) {
notFound();
}
return <article>{/* ... */}</article>;
}template.tsx
Simile a layout.tsx ma:
- Si re-mounta su ogni navigazione
- Mantiene stato/UI separati tra le route
// app/dashboard/template.tsx
export default function Template({
children,
}: {
children: React.ReactNode;
}) {
return <div className="animate-fade-in">{children}</div>;
}default.tsx
Fallback per parallel routes quando non c'è match.
// app/dashboard/@team/default.tsx
export default function Default() {
return <div>Select a team member</div>;
}Navigation
Link Component
import Link from "next/link";
export default function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog/[slug]" as="/blog/hello-world">
Post
</Link>
</nav>
);
}useRouter Hook (Client Components)
"use client";
import { useRouter } from "next/navigation";
export default function NavigationButton() {
const router = useRouter();
return (
<button onClick={() => router.push("/dashboard")}>
Go to Dashboard
</button>
);
}| Method | Description |
|---|---|
push(href) | Naviga a nuova route |
replace(href) | Naviga sostituendo la route corrente |
back() | Torna indietro nella history |
forward() | Vai avanti nella history |
refresh() | Refresh della route corrente |
prefetch(href) | Prefetch di una route |
Programmatic Navigation (Server Components)
import { redirect } from "next/navigation";
export default async function ProtectedPage() {
const session = await getSession();
if (!session) {
redirect("/login");
}
return <div>Protected content</div>;
}API Route Handlers
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
// GET /api/users
export async function GET(request: NextRequest) {
const users = await db.user.findMany();
return NextResponse.json(users);
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}// app/api/users/[id]/route.ts
interface RouteParams {
params: Promise<{ id: string }>;
}
// GET /api/users/[id]
export async function GET(request: NextRequest, { params }: RouteParams) {
const { id } = await params;
const user = await db.user.findUnique({ where: { id } });
if (!user) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(user);
}
// PATCH /api/users/[id]
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { id } = await params;
const body = await request.json();
const user = await db.user.update({
where: { id },
data: body,
});
return NextResponse.json(user);
}
// DELETE /api/users/[id]
export async function DELETE(request: NextRequest, { params }: RouteParams) {
const { id } = await params;
await db.user.delete({ where: { id } });
return NextResponse.json(null, { status: 204 });
}next/image
import Image from "next/image";
export default function Avatar({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={64}
height={64}
priority // Per LCP images
quality={80} // 1-100, default 75
placeholder="blur" // o "empty"
blurDataURL="data:image/jpeg;base64,..." // Placeholder base64
className="rounded-full"
/>
);
}Fill Mode
<Image
src="/photo.jpg"
alt="Photo"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>next/font
// 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"],
variable: "--font-roboto-mono",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body className={inter.className}>{children}</body>
</html>
);
}/* globals.css */
body {
font-family: var(--font-inter), system-ui, sans-serif;
}
code {
font-family: var(--font-roboto-mono), monospace;
}Environment Variables
# .env.local
# Accessibile solo nel server
DATABASE_URL="postgresql://..."
API_SECRET_KEY="secret"
# Accessibile anche nel client (prefisso NEXT_PUBLIC_)
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_APP_NAME="My App"// Server Component - accesso diretto
export default async function Page() {
const data = await fetch(process.env.DATABASE_URL!);
// ...
}// Client Component - solo NEXT_PUBLIC_
"use client";
export default function Config() {
return <div>API: {process.env.NEXT_PUBLIC_API_URL}</div>;
}Next.js App Router - Best Practices
This document contains comprehensive best practices, constraints, and warnings for Next.js 16+ App Router development.
Table of Contents
- Architecture Best Practices
- Performance Best Practices
- Security Best Practices
- Server vs Client Component Guidelines
- Data Fetching Best Practices
- Caching Best Practices
- Constraints and Limitations
- Common Pitfalls and Warnings
---
Architecture Best Practices
1. Component Hierarchy Strategy
Organize components by data requirements:
// ✅ Good: Server Component at the top
async function Page() {
const data = await fetchData();
return (
<div>
<Header data={data} />
<ClientComponent initialData={data} />
</div>
);
}
// ❌ Bad: Making everything client components
"use client";
function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>...</div>;
}Push Client Components down the tree:
// app/page.tsx - Server Component
import { ProductList } from "@/components/ProductList";
export default async function Page() {
const products = await getProducts();
return <ProductList products={products} />;
}
// components/ProductList.tsx - Server Component
export function ProductList({ products }: { products: Product[] }) {
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// components/ProductCard.tsx - Client Component (only because of onClick)
"use client";
export function ProductCard({ product }: { product: Product }) {
return (
<div onClick={() => console.log(product.id)}>
<h3>{product.name}</h3>
</div>
);
}2. File Organization
Follow Next.js conventions:
app/
├── (auth)/ # Route group (no URL prefix)
│ ├── login/
│ │ └── page.tsx
│ ├── register/
│ │ └── page.tsx
│ └── layout.tsx # Shared auth layout
├── (dashboard)/ # Route group
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx # /dashboard
│ ├── @stats/ # Parallel route slot
│ │ └── page.tsx
│ └── @notifications/ # Parallel route slot
│ └── page.tsx
├── api/ # API routes
│ └── users/
│ └── route.ts
├── blog/
│ ├── [slug]/ # Dynamic route
│ │ └── page.tsx
│ └── page.tsx
├── globals.css
├── layout.tsx # Root layout
└── page.tsx # Home page3. Layout Strategy
Use layouts efficiently:
// Root layout - wraps entire app
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
// Route group layout - wraps dashboard routes
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}---
Performance Best Practices
1. Optimize Bundle Size
Avoid unnecessary client components:
// ❌ Bad: Client component when not needed
"use client";
export function StaticContent() {
return <div>This never changes</div>;
}
// ✅ Good: Server component (default)
export function StaticContent() {
return <div>This never changes</div>;
}Code split client components:
"use client";
import dynamic from "next/dynamic";
// Dynamically import heavy component
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <ChartSkeleton />,
ssr: false, // Skip SSR if not needed
});
export function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<HeavyChart />
</div>
);
}2. Image Optimization
Always use next/image:
import Image from "next/image";
export function ProductImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={500}
height={500}
priority // For above-the-fold images
placeholder="blur" // Or "blur"
/>
);
}3. Font Optimization
Use next/font:
import { Inter, Roboto } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
const roboto = Roboto({
weight: ["400", "700"],
subsets: ["latin"],
variable: "--font-roboto",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html className={`${inter.variable} ${roboto.variable}`}>
<body>{children}</body>
</html>
);
}4. Loading States
Use loading.tsx for Suspense boundaries:
// app/blog/loading.tsx
export default function BlogLoading() {
return (
<div className="blog-loading">
<div className="skeleton-header" />
<div className="skeleton-grid">
{[1, 2, 3, 4, 5, 6].map(i => (
<div key={i} className="skeleton-card" />
))}
</div>
</div>
);
}Use Suspense for granular loading:
import { Suspense } from "react";
export default async function Page() {
return (
<div>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}5. Streaming and Progressive Rendering
Leverage async APIs for streaming:
import { Suspense } from "react";
export default async function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Stream in data as it becomes available */}
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
);
}---
Security Best Practices
1. Server Actions Security
Always validate and sanitize input:
"use server";
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(["user", "admin"]).default("user"),
});
export async function createUser(formData: FormData) {
// Validate input
const rawData = {
name: formData.get("name"),
email: formData.get("email"),
role: formData.get("role"),
};
const result = CreateUserSchema.safeParse(rawData);
if (!result.success) {
return { errors: result.error.flatten().fieldErrors };
}
// Use validated data
const user = await db.user.create({
data: result.data,
});
return { success: true, user };
}Implement rate limiting:
// app/actions/rate-limiter.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
});
export async function checkRateLimit(identifier: string) {
const { success, remaining } = await ratelimit.limit(identifier);
if (!success) {
throw new Error("Rate limit exceeded");
}
return remaining;
}
// Usage in server action
"use server";
import { checkRateLimit } from "./rate-limiter";
import { auth } from "@/lib/auth";
export async function createUser(formData: FormData) {
const session = await auth();
if (!session?.user?.id) {
throw new Error("Unauthorized");
}
await checkRateLimit(session.user.id);
// Proceed with user creation
}Implement authentication checks:
"use server";
import { auth } from "@/lib/auth";
export async function sensitiveAction(formData: FormData) {
const session = await auth();
if (!session) {
return { error: "Unauthorized" };
}
// Check user permissions
if (session.user.role !== "admin") {
return { error: "Forbidden" };
}
// Perform action
}2. External Data Fetching
Always validate external API responses:
// app/products/page.tsx
import { z } from "zod";
// Schema for external API response
const ProductSchema = z.object({
id: z.string(),
name: z.string(),
price: z.number(),
description: z.string(),
});
const ProductsResponseSchema = z.object({
products: z.array(ProductSchema),
total: z.number(),
});
async function getProducts() {
const apiUrl = process.env.EXTERNAL_API_URL; // Use environment variable
const res = await fetch(`${apiUrl}/products`);
if (!res.ok) {
throw new Error("Failed to fetch products");
}
const rawData = await res.json();
// Validate and parse response
const result = ProductsResponseSchema.safeParse(rawData);
if (!result.success) {
throw new Error("Invalid API response format");
}
return result.data;
}Sanitize user-generated content:
import DOMPurify from "isomorphic-dompurify";
export function renderUserContent(content: string) {
const sanitized = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
ALLOWED_ATTR: ["href"],
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}3. Environment Variables
Never expose secrets:
// ✅ Good: Use server-side only
async function ServerComponent() {
const apiKey = process.env.API_KEY; // Only available on server
const data = await fetchData(apiKey);
return <DataDisplay data={data} />;
}
// ❌ Bad: Exposing secret to client
"use client";
function ClientComponent() {
const apiKey = process.env.API_KEY; // Exposed to browser
// ...
}Prefix public variables:
# .env.local
API_KEY=secret_key_xxx # Server-only
NEXT_PUBLIC_API_URL=https://... # Exposed to client---
Server vs Client Component Guidelines
Decision Tree
Start with Server Component (default). Convert to Client Component if:
1. Needs React hooks
"use client";
import { useState, useEffect } from "react";2. Needs browser APIs
"use client";
useEffect(() => {
const width = window.innerWidth;
}, []);3. Needs event handlers
"use client";
<button onClick={handleClick}>Click me</button>4. Needs client-only libraries
"use client";
import { Chart } from "chart.js";Common Patterns
Interactive component with server data:
// Server Component
async function ProductPage({ params }: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProduct(id);
return <AddToCartButton productId={product.id} />;
}
// Client Component
"use client";
import { useState } from "react";
export function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? "Added!" : "Add to Cart"}
</button>
);
}---
Data Fetching Best Practices
1. Fetch in Server Components
// ✅ Good: Fetch in Server Component
async function UserPage({ params }: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const user = await getUser(id);
return <UserProfile user={user} />;
}
// ❌ Bad: Fetch in Client Component
"use client";
function UserPage({ id }: { id: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
getUser(id).then(setUser);
}, [id]);
// ...
}2. Parallelize Independent Fetches
// ✅ Good: Parallel fetching with Promise.all
export default async function Dashboard() {
const [users, posts, comments] = await Promise.all([
getUsers(),
getPosts(),
getComments(),
]);
return <DashboardData users={users} posts={posts} comments={comments} />;
}
// ❌ Bad: Sequential fetching
export default async function Dashboard() {
const users = await getUsers();
const posts = await getPosts();
const comments = await getComments();
// ...
}3. Use React's cache() for Deduplication
import { cache } from "react";
const getSingleUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
// Multiple components can call this without duplicate fetches
async function Component1() {
const user = await getSingleUser("123");
// ...
}
async function Component2() {
const user = await getSingleUser("123"); // Deduped!
// ...
}4. Add Suspense Boundaries
import { Suspense } from "react";
export default async function Page() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<User />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</div>
);
}---
Caching Best Practices
1. Use "use cache" for Static Content
"use cache";
import { cacheLife } from "next/cache";
export default async function ProductList() {
cacheLife("days");
const products = await getProducts();
return <ProductsDisplay products={products} />;
}2. Use Tags for Revalidation
"use cache";
import { cacheTag } from "next/cache";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
cacheTag(`product-${id}`, "products");
const product = await getProduct(id);
return <ProductDetail product={product} />;
}3. Implement Revalidation on Updates
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProduct(id: string, data: any) {
await db.product.update({
where: { id },
data,
});
// Revalidate specific product
revalidateTag(`product-${id}`);
// Revalidate all products
revalidateTag("products");
// Revalidate by path
revalidatePath(`/products/${id}`);
}---
Constraints and Limitations
Server Components Constraints
Cannot use:
- React hooks (useState, useEffect, useContext, etc.)
- Browser APIs (window, document, localStorage, etc.)
- Event handlers (onClick, onSubmit, etc.)
- Client-only libraries
// ❌ Invalid Server Component
export default function BadComponent() {
const [count, setCount] = useState(0); // Error!
useEffect(() => { }, []); // Error!
return <button onClick={() => setCount(1)}>Click</button>; // Error!
}Cannot be async:
// ❌ Invalid Client Component
"use client";
export default async function BadComponent() {
const data = await fetchData(); // Error!
return <div>{data}</div>;
}Next.js 16 Async API Constraints
All Next.js APIs are async:
import { cookies, headers, draftMode } from "next/headers";
// ❌ Wrong (will return Promise object)
export default function Page() {
const cookieStore = cookies(); // Returns Promise!
// ...
}
// ✅ Correct
export default async function Page() {
const cookieStore = await cookies(); // Await the Promise
// ...
}Params are Promise-based:
// ❌ Wrong
export default function Page({
params,
}: {
params: { id: string };
}) {
const { id } = params; // Error!
}
// ✅ Correct
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params; // Await the Promise
}---
Common Pitfalls and Warnings
1. Forgetting to await Async APIs
// ❌ Wrong
export default async function Page() {
const cookieStore = cookies(); // Forgot await
const session = cookieStore.get("session"); // Will be undefined!
// ...
}
// ✅ Correct
export default async function Page() {
const cookieStore = await cookies(); // Await it
const session = cookieStore.get("session"); // Works!
// ...
}2. Using Browser APIs in Server Components
// ❌ Wrong
export default function Page() {
const width = window.innerWidth; // Runtime error!
return <div>Width: {width}</div>;
}
// ✅ Correct
"use client";
export default function Page() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <div>Width: {width}</div>;
}3. Server Actions Without Validation
// ❌ Wrong - No validation
"use server";
export async function createUser(formData: FormData) {
const name = formData.get("name"); // Could be anything!
const email = formData.get("email"); // Not validated!
await db.user.create({ data: { name, email } });
}
// ✅ Correct - With validation
"use server";
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
export async function createUser(formData: FormData) {
const rawData = {
name: formData.get("name"),
email: formData.get("email"),
};
const result = CreateUserSchema.safeParse(rawData);
if (!result.success) {
return { errors: result.error.flatten().fieldErrors };
}
await db.user.create({ data: result.data });
}4. Exposing Secrets to Client
// ❌ Wrong - Exposes secret
"use client";
export function ApiComponent() {
const apiKey = process.env.API_KEY; // Visible in browser!
// ...
}
// ✅ Correct - Server-side only
async function ServerComponent() {
const apiKey = process.env.API_KEY; // Safe
const data = await fetchData(apiKey);
return <ClientComponent data={data} />;
}5. Invalid Client Component Data Fetching
// ❌ Wrong - Direct data fetching in Client Component
"use client";
export default function UserPage({ id }: { id: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(setUser);
}, [id]);
if (!user) return <div>Loading...</div>;
return <UserProfile user={user} />;
}
// ✅ Correct - Server Component data fetching
async function UserPage({ params }: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const user = await getUser(id);
return <UserProfile user={user} />;
}6. Missing Error Boundaries
// ❌ Wrong - No error handling
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const data = await fetchData(id); // Could throw!
return <DataDisplay data={data} />;
}
// ✅ Correct - With error.tsx
// app/error.tsx
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}---
See Also
Caching Strategies (Next.js 16+)
Explicit Caching with "use cache"
Next.js 16 introduce la cache esplicita con la direttiva "use cache".
Basic Usage
"use cache";
export default async function ProductPage() {
const products = await fetchProducts();
return <ProductList products={products} />;
}Cache Life Configuration
import { cacheLife } from "next/cache";
"use cache";
export default async function ProductPage() {
cacheLife("hours"); // Predefined profile
const products = await fetchProducts();
return <ProductList products={products} />;
}Predefined Cache Profiles
| Profile | Durata | stale | revalidate |
|---|---|---|---|
"seconds" | 1s | 0 | auto |
"minutes" | 1m | 0 | auto |
"hours" | 1h | 0 | auto |
"days" | 1d | 0 | auto |
"weeks" | 1w | 0 | auto |
"max" | 1y | 0 | auto |
Custom Cache Profile (next.config.ts)
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
cacheLife: {
blog: {
stale: 3600, // 1 hour
revalidate: 900, // 15 minutes
expire: 86400, // 24 hours
},
product: {
stale: 60,
revalidate: 30,
expire: 3600,
},
},
},
};
export default nextConfig;"use cache";
import { cacheLife } from "next/cache";
export default async function BlogPost() {
cacheLife("blog");
// ...
}Cache Tags for Revalidation
"use cache";
import { cacheTag } from "next/cache";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
cacheTag(`product-${id}`);
cacheTag("products");
const product = await fetchProduct(id);
return <ProductDetail product={product} />;
}On-Demand Revalidation
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const { tag } = await request.json();
revalidateTag(tag);
return NextResponse.json({ revalidated: true });
}Cache per Function
import { cache } from "react";
// React cache deduplicates requests
const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
export default async function Profile({ userId }: { userId: string }) {
const user = await getUser(userId);
// ...
}Disabling Cache
export const dynamic = "force-dynamic";
export const revalidate = 0;
export default async function RealtimeData() {
const data = await fetchData({ cache: "no-store" });
// ...
}Route Segment Config
// app/dashboard/page.tsx
export const dynamic = "auto"; // 'auto' | 'force-dynamic' | 'force-static' | 'error'
export const revalidate = 3600; // seconds
export const fetchCache = "auto"; // 'auto' | 'default-cache' | 'only-no-store' | 'force-cache' | 'force-no-store'
export const runtime = "nodejs"; // 'nodejs' | 'edge'
export const preferredRegion = "iad1"; // 'auto' | 'global' | 'home' | string
export default async function DashboardPage() {
// ...
}Data Fetching Patterns
Server Components Fetching
// app/users/page.tsx
async function getUsers() {
const res = await fetch(`${process.env.API_URL}/users`, {
// Cache configuration
next: { revalidate: 3600, tags: ["users"] },
});
if (!res.ok) throw new Error("Failed to fetch users");
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Database Query (Server Component)
// app/posts/page.tsx
import { db } from "@/lib/db";
async function getPosts() {
return db.post.findMany({
where: { published: true },
include: { author: true },
orderBy: { createdAt: "desc" },
});
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<main>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
</article>
))}
</main>
);
}Parallel Data Fetching
// app/dashboard/page.tsx
import { Suspense } from "react";
async function getRevenue() {
// Simulated delay
await new Promise((resolve) => setTimeout(resolve, 1000));
return { total: 50000 };
}
async function getOrders() {
await new Promise((resolve) => setTimeout(resolve, 1500));
return [{ id: 1, amount: 100 }];
}
async function RevenueCard() {
const revenue = await getRevenue();
return <div>Revenue: ${revenue.total}</div>;
}
async function OrdersCard() {
const orders = await getOrders();
return <div>Orders: {orders.length}</div>;
}
export default function DashboardPage() {
return (
<div>
<Suspense fallback={<p>Loading revenue...</p>}>
<RevenueCard />
</Suspense>
<Suspense fallback={<p>Loading orders...</p>}>
<OrdersCard />
</Suspense>
</div>
);
}Sequential Data Fetching (Dependent)
// app/user/[id]/posts/page.tsx
async function getUser(id: string) {
const res = await fetch(`${process.env.API_URL}/users/${id}`);
return res.json();
}
async function getPosts(userId: string) {
const res = await fetch(
`${process.env.API_URL}/users/${userId}/posts`
);
return res.json();
}
export default async function UserPostsPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Sequential: attendi l'utente prima di fetchare i post
const user = await getUser(id);
const posts = await getPosts(user.id);
return (
<div>
<h1>Posts by {user.name}</h1>
{posts.map((post) => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}Request Memoization (React cache)
import { cache } from "react";
// Stessa richiesta deduplicata automaticamente
const getUser = cache(async (id: string) => {
console.log("Fetching user", id); // Chiama una sola volta per richiesta
return db.user.findUnique({ where: { id } });
});
// Componente 1
async function UserProfile({ id }: { id: string }) {
const user = await getUser(id);
return <div>{user?.name}</div>;
}
// Componente 2 (stessa richiesta, deduplicata)
async function UserAvatar({ id }: { id: string }) {
const user = await getUser(id); // Non fetcha di nuovo
return <img src={user?.avatar} />;
}
// Pagina parent
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<>
<UserProfile id={id} />
<UserAvatar id={id} />
</>
);
}Error Handling
// app/error.tsx
"use client";
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}// app/users/page.tsx
async function getUsers() {
try {
const res = await fetch(`${process.env.API_URL}/users`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
} catch (error) {
// Log per monitoring
console.error("Failed to fetch users:", error);
// Rethrow per attivare error.tsx
throw new Error("Unable to load users. Please try again later.");
}
}Loading UI
// app/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
<div className="space-y-2">
{[...Array(3)].map((_, i) => (
<div key={i} className="h-8 bg-gray-200 rounded" />
))}
</div>
</div>
);
}Streaming with Suspense
// app/components/Skeleton.tsx
export function CardSkeleton() {
return (
<div className="animate-pulse p-4 border rounded">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-full"></div>
</div>
);
}// app/page.tsx
import { Suspense } from "react";
import { CardSkeleton } from "./components/Skeleton";
import { ProductList } from "./components/ProductList";
export default function HomePage() {
return (
<div>
<header>
<h1>Welcome</h1>
</header>
{/> Static content renders immediately </}
<section>About our store...</section>
{/> Dynamic content streams in </}
<Suspense fallback={<CardSkeleton />}>
<ProductList />
</Suspense>
</div>
);
}Next.js App Router - Detailed Examples
This document contains comprehensive, real-world examples for Next.js 16+ App Router development.
Table of Contents
- Example 1: Blog with Server Actions
- Example 2: E-commerce Product Page with Caching
- Example 3: Dashboard with Parallel Routes
- Example 4: Image Gallery with Infinite Scroll
- Example 5: Real-time Chat App
---
Example 1: Blog with Server Actions
Complete blog platform with post creation, validation, and optimistic updates.
Architecture
app/
├── blog/
│ ├── page.tsx # Blog listing
│ ├── [slug]/
│ │ └── page.tsx # Single post
│ ├── new/
│ │ └── page.tsx # Create post form
│ └── actions.ts # Server actions
├── components/
│ ├── PostCard.tsx # Client component
│ └── CommentForm.tsx # Comment form
└── lib/
└── db.ts # Database clientServer Actions with Validation
// app/blog/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
// Validation schemas
const CreatePostSchema = z.object({
title: z.string()
.min(5, "Title must be at least 5 characters")
.max(100, "Title must not exceed 100 characters"),
content: z.string()
.min(10, "Content must be at least 10 characters")
.max(10000, "Content must not exceed 10000 characters"),
excerpt: z.string().max(200).optional(),
published: z.boolean().default(false),
});
const CreateCommentSchema = z.object({
postId: z.string().uuid(),
author: z.string().min(2, "Author name required"),
content: z.string().min(5, "Comment must be at least 5 characters"),
});
// Create post action
export async function createPost(prevState: any, formData: FormData) {
const rawData = {
title: formData.get("title"),
content: formData.get("content"),
excerpt: formData.get("excerpt"),
published: formData.get("published") === "true",
};
const result = CreatePostSchema.safeParse(rawData);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
message: "Validation failed",
};
}
try {
const post = await db.post.create({
data: {
...result.data,
slug: generateSlug(result.data.title),
authorId: getCurrentUserId(),
},
});
revalidatePath("/blog");
revalidatePath("/blog/new");
return {
success: true,
post,
message: "Post created successfully",
};
} catch (error) {
return {
success: false,
message: "Failed to create post",
errors: { _form: ["Database error occurred"] },
};
}
}
// Create comment action with optimistic update support
export async function createComment(prevState: any, formData: FormData) {
const rawData = {
postId: formData.get("postId"),
author: formData.get("author"),
content: formData.get("content"),
};
const result = CreateCommentSchema.safeParse(rawData);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
};
}
try {
const comment = await db.comment.create({
data: result.data,
});
revalidatePath(`/blog/${rawData.postId}`);
return {
success: true,
comment,
};
} catch (error) {
return {
success: false,
message: "Failed to create comment",
};
}
}
// Delete post action
export async function deletePost(formData: FormData) {
const id = formData.get("id") as string;
await db.post.delete({
where: { id },
});
revalidatePath("/blog");
redirect("/blog");
}
// Helper function to generate URL-friendly slugs
function generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
function getCurrentUserId(): string {
// Implement authentication logic
return "user-id";
}Blog Listing Page
// app/blog/page.tsx
import Link from "next/link";
import { Suspense } from "react";
async function getPosts() {
const posts = await db.post.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
include: {
author: {
select: { name: true },
},
_count: {
select: { comments: true },
},
},
});
return posts;
}
export default async function BlogPage() {
return (
<div className="blog-container">
<header className="blog-header">
<h1>Blog</h1>
<Link href="/blog/new" className="btn-primary">
New Post
</Link>
</header>
<Suspense fallback={<PostsSkeleton />}>
<PostsList />
</Suspense>
</div>
);
}
async function PostsList() {
const posts = await getPosts();
return (
<div className="posts-grid">
{posts.map(post => (
<article key={post.id} className="post-card">
<Link href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
{post.excerpt && <p>{post.excerpt}</p>}
</Link>
<div className="post-meta">
<span>By {post.author.name}</span>
<time>{new Date(post.createdAt).toLocaleDateString()}</time>
<span>{post._count.comments} comments</span>
</div>
</article>
))}
</div>
);
}
function PostsSkeleton() {
return (
<div className="posts-grid">
{[1, 2, 3, 4, 5, 6].map(i => (
<div key={i} className="post-card skeleton">
<div className="skeleton-title" />
<div className="skeleton-excerpt" />
<div className="skeleton-meta" />
</div>
))}
</div>
);
}Create Post Form with Client Components
// app/blog/new/page.tsx
"use client";
import { useActionState } from "react";
import { useRouter } from "next/navigation";
import { createPost } from "../actions";
export default function NewPostPage() {
const router = useRouter();
const [state, formAction, pending] = useActionState(createPost, null);
// Redirect on success
if (state?.success) {
router.push(`/blog/${state.post.slug}`);
router.refresh();
}
return (
<div className="container">
<h1>Create New Post</h1>
<form action={formAction} className="post-form">
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
name="title"
disabled={pending}
aria-invalid={!!state?.errors?.title}
/>
{state?.errors?.title && (
<span className="error">{state.errors.title[0]}</span>
)}
</div>
<div className="form-group">
<label htmlFor="excerpt">Excerpt (optional)</label>
<textarea
id="excerpt"
name="excerpt"
rows={2}
disabled={pending}
/>
</div>
<div className="form-group">
<label htmlFor="content">Content</label>
<textarea
id="content"
name="content"
rows={15}
disabled={pending}
aria-invalid={!!state?.errors?.content}
/>
{state?.errors?.content && (
<span className="error">{state.errors.content[0]}</span>
)}
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="published"
value="true"
disabled={pending}
/>
Publish immediately
</label>
</div>
{state?.message && !state.success && (
<div className="alert alert-error">{state.message}</div>
)}
<div className="form-actions">
<button
type="submit"
disabled={pending}
className="btn-primary"
>
{pending ? "Creating..." : "Create Post"}
</button>
<button
type="button"
disabled={pending}
onClick={() => router.back()}
className="btn-secondary"
>
Cancel
</button>
</div>
</form>
</div>
);
}Single Post with Comments
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { Suspense } from "react";
import { CommentSection } from "./CommentSection";
async function getPost(slug: string) {
const post = await db.post.findUnique({
where: { slug },
include: {
author: {
select: { name: true, email: true },
},
comments: {
orderBy: { createdAt: "desc" },
},
},
});
return post;
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
notFound();
}
return (
<article className="post">
<header className="post-header">
<h1>{post.title}</h1>
<div className="post-meta">
<span>By {post.author.name}</span>
<time>
{new Date(post.createdAt).toLocaleDateString()}
</time>
</div>
</header>
{post.excerpt && (
<p className="post-excerpt">{post.excerpt}</p>
)}
<div className="post-content">
{post.content}
</div>
<Suspense fallback={<CommentsSkeleton />}>
<CommentSection postId={post.id} comments={post.comments} />
</Suspense>
</article>
);
}
function CommentsSkeleton() {
return (
<div className="comments-section">
<h2>Comments</h2>
<div className="comment-skeletons">
{[1, 2, 3].map(i => (
<div key={i} className="comment skeleton" />
))}
</div>
</div>
);
}Comment Section with Optimistic Updates
// app/blog/[slug]/CommentSection.tsx
"use client";
import { useOptimistic, useActionState } from "react";
import { createComment } from "../../actions";
interface Comment {
id: string;
author: string;
content: string;
createdAt: Date;
}
export function CommentSection({
postId,
comments,
}: {
postId: string;
comments: Comment[];
}) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment: Comment) => [
{ ...newComment, id: "temp", createdAt: new Date() },
...state,
]
);
const [state, formAction, pending] = useActionState(
createComment.bind(null, postId),
null
);
async function handleSubmit(formData: FormData) {
formData.append("postId", postId);
const author = formData.get("author") as string;
const content = formData.get("content") as string;
addOptimisticComment({ author, content } as Comment);
formAction(formData);
}
return (
<section className="comments-section">
<h2>Comments ({optimisticComments.length})</h2>
<form action={handleSubmit} className="comment-form">
<input
type="text"
name="author"
placeholder="Your name"
required
disabled={pending}
/>
<textarea
name="content"
placeholder="Write a comment..."
rows={3}
required
disabled={pending}
/>
<button type="submit" disabled={pending}>
{pending ? "Posting..." : "Post Comment"}
</button>
</form>
<div className="comments-list">
{optimisticComments.map(comment => (
<div key={comment.id} className="comment">
<div className="comment-header">
<strong>{comment.author}</strong>
<time>
{new Date(comment.createdAt).toLocaleString()}
</time>
</div>
<p>{comment.content}</p>
</div>
))}
</div>
</section>
);
}---
Example 2: E-commerce Product Page with Caching
High-performance product catalog with intelligent caching and revalidation.
Product Page with Advanced Caching
// app/products/[id]/page.tsx
"use cache";
import { cacheLife, cacheTag } from "next/cache";
import { notFound } from "next/navigation";
import { Suspense } from "react";
async function getProduct(id: string) {
const product = await db.product.findUnique({
where: { id },
include: {
category: true,
variants: true,
reviews: {
take: 10,
orderBy: { createdAt: "desc" },
},
},
});
return product;
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Tag for selective revalidation
cacheTag(`product-${id}`, "products", "catalog");
// Cache for 1 hour with stale-while-revalidate
cacheLife({
stale: 60,
revalidate: 3600,
});
const product = await getProduct(id);
if (!product) {
notFound();
}
return (
<div className="product-page">
<ProductDetail product={product} />
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={product.id} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<RelatedProducts
categoryId={product.category.id}
currentProductId={product.id}
/>
</Suspense>
</div>
);
}
// Separate component with different cache strategy
async function RelatedProducts({
categoryId,
currentProductId,
}: {
categoryId: string;
currentProductId: string;
}) {
"use cache";
cacheTag("related-products", `category-${categoryId}`);
cacheLife("days");
const products = await db.product.findMany({
where: {
categoryId,
id: { not: currentProductId },
},
take: 4,
});
return (
<section className="related-products">
<h2>You might also like</h2>
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</section>
);
}Revalidation API Endpoint
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from "next/cache";
import { NextResponse } from "next/server";
import { z } from "zod";
const RevalidateSchema = z.object({
type: z.enum(["tag", "path"]),
value: z.string(),
secret: z.string(),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const result = RevalidateSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: "Invalid request" },
{ status: 400 }
);
}
// Verify secret to prevent unauthorized revalidation
if (result.data.secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
const { type, value } = result.data;
if (type === "tag") {
revalidateTag(value);
} else {
revalidatePath(value);
}
return NextResponse.json({
revalidated: true,
now: Date.now(),
});
} catch (error) {
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}Server Action for Product Updates with Revalidation
// app/actions/products.ts
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
import { z } from "zod";
const UpdateProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
description: z.string(),
price: z.number().positive(),
stock: z.number().int().min(0),
});
export async function updateProduct(prevState: any, formData: FormData) {
const rawData = {
id: formData.get("id"),
name: formData.get("name"),
description: formData.get("description"),
price: Number(formData.get("price")),
stock: Number(formData.get("stock")),
};
const result = UpdateProductSchema.safeParse(rawData);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
};
}
try {
const product = await db.product.update({
where: { id: result.data.id },
data: result.data,
});
// Revalidate specific product
revalidateTag(`product-${product.id}`);
// Revalidate product listings
revalidateTag("products");
// Revalidate category pages
revalidateTag(`category-${product.categoryId}`);
// Revalidate paths
revalidatePath(`/products/${product.id}`);
revalidatePath("/products");
return {
success: true,
product,
};
} catch (error) {
return {
success: false,
message: "Failed to update product",
};
}
}
export async function updateInventory(
productId: string,
quantity: number
) {
await db.product.update({
where: { id: productId },
data: {
stock: { increment: -quantity },
},
});
// Immediate revalidation for inventory-critical pages
revalidateTag(`product-${productId}`);
revalidateTag("low-stock");
}---
Example 3: Dashboard with Parallel Routes
Complex dashboard with sidebar, multiple panels, and independent data loading.
Dashboard Layout Structure
// app/dashboard/layout.tsx
import Link from "next/link";
import { Sidebar } from "@/components/Sidebar";
export default function DashboardLayout({
children,
sidebar,
stats,
notifications,
}: {
children: React.ReactNode;
sidebar: React.ReactNode;
stats: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div className="dashboard-layout">
<aside className="dashboard-sidebar">
{sidebar}
</aside>
<main className="dashboard-main">
<header className="dashboard-header">
<h1>Dashboard</h1>
<nav className="dashboard-nav">
<Link href="/dashboard/overview">Overview</Link>
<Link href="/dashboard/analytics">Analytics</Link>
<Link href="/dashboard/settings">Settings</Link>
</nav>
</header>
<div className="dashboard-panels">
<div className="panel stats-panel">
{stats}
</div>
<div className="panel notifications-panel">
{notifications}
</div>
</div>
<div className="dashboard-content">
{children}
</div>
</main>
</div>
);
}Sidebar Slot
// app/dashboard/@sidebar/default.tsx
export default function Sidebar() {
return (
<nav className="sidebar">
<Link href="/dashboard" className="sidebar-logo">
Dashboard
</Link>
<ul className="sidebar-menu">
<li>
<Link href="/dashboard/overview">
Overview
</Link>
</li>
<li>
<Link href="/dashboard/analytics">
Analytics
</Link>
</li>
<li>
<Link href="/dashboard/reports">
Reports
</Link>
</li>
<li>
<Link href="/dashboard/settings">
Settings
</Link>
</li>
</ul>
</nav>
);
}Stats Slot with Real-time Data
// app/dashboard/@stats/page.tsx
import { Suspense } from "react";
async function getDashboardStats() {
const [users, orders, revenue] = await Promise.all([
db.user.count(),
db.order.count(),
db.order.aggregate({
_sum: { total: true },
}),
]);
return {
users,
orders,
revenue: revenue._sum.total || 0,
};
}
export default async function StatsPanel() {
return (
<div className="stats-panel">
<h2>Statistics</h2>
<Suspense fallback={<StatsSkeleton />}>
<StatsData />
</Suspense>
</div>
);
}
async function StatsData() {
const stats = await getDashboardStats();
return (
<div className="stats-grid">
<div className="stat-card">
<p className="stat-label">Total Users</p>
<p className="stat-value">{stats.users}</p>
</div>
<div className="stat-card">
<p className="stat-label">Total Orders</p>
<p className="stat-value">{stats.orders}</p>
</div>
<div className="stat-card">
<p className="stat-label">Revenue</p>
<p className="stat-value">${stats.revenue.toLocaleString()}</p>
</div>
</div>
);
}
function StatsSkeleton() {
return (
<div className="stats-grid">
{[1, 2, 3].map(i => (
<div key={i} className="stat-card skeleton">
<div className="skeleton-label" />
<div className="skeleton-value" />
</div>
))}
</div>
);
}Notifications Slot
// app/dashboard/@notifications/page.tsx
import { Suspense } from "react";
async function getNotifications() {
const notifications = await db.notification.findMany({
where: { userId: getCurrentUserId() },
orderBy: { createdAt: "desc" },
take: 5,
});
return notifications;
}
export default async function NotificationsPanel() {
return (
<div className="notifications-panel">
<h2>Notifications</h2>
<Suspense fallback={<NotificationsSkeleton />}>
<NotificationsList />
</Suspense>
</div>
);
}
async function NotificationsList() {
const notifications = await getNotifications();
if (notifications.length === 0) {
return <p>No new notifications</p>;
}
return (
<ul className="notifications-list">
{notifications.map(notification => (
<li
key={notification.id}
className={notification.read ? "read" : "unread"}
>
<p>{notification.message}</p>
<time>
{new Date(notification.createdAt).toLocaleString()}
</time>
</li>
))}
</ul>
);
}
function NotificationsSkeleton() {
return (
<ul className="notifications-list">
{[1, 2, 3, 4, 5].map(i => (
<li key={i} className="notification skeleton" />
))}
</ul>
);
}
function getCurrentUserId(): string {
// Implement authentication logic
return "user-id";
}Main Content Area
// app/dashboard/overview/page.tsx
import { Suspense } from "react";
async function getRecentActivity() {
const activities = await db.activity.findMany({
orderBy: { createdAt: "desc" },
take: 10,
});
return activities;
}
export default async function OverviewPage() {
return (
<div className="overview-page">
<h2>Overview</h2>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
);
}
async function RecentActivity() {
const activities = await getRecentActivity();
return (
<section className="recent-activity">
<h3>Recent Activity</h3>
<ul className="activity-list">
{activities.map(activity => (
<li key={activity.id}>
<span className="activity-type">{activity.type}</span>
<span className="activity-description">
{activity.description}
</span>
<time>
{new Date(activity.createdAt).toLocaleString()}
</time>
</li>
))}
</ul>
</section>
);
}---
Example 4: Image Gallery with Infinite Scroll
Photo gallery with progressive loading and infinite scroll pagination.
Gallery Listing
// app/gallery/page.tsx
import { ImageGrid } from "@/components/ImageGrid";
import { LoadMoreButton } from "@/components/LoadMoreButton";
import { getImages } from "@/lib/images";
const PAGE_SIZE = 12;
export default async function GalleryPage({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}) {
const { page = "1" } = await searchParams;
const currentPage = parseInt(page);
const { images, totalPages } = await getImages({
page: currentPage,
limit: PAGE_SIZE,
});
return (
<div className="gallery-page">
<h1>Image Gallery</h1>
<ImageGrid images={images} />
{currentPage < totalPages && (
<LoadMoreButton
currentPage={currentPage}
totalPages={totalPages}
/>
)}
</div>
);
}Client-Side Infinite Scroll Component
// components/LoadMoreButton.tsx
"use client";
import { useRouter, usePathname } from "next/navigation";
import { useEffect, useState, useRef } from "react";
export function LoadMoreButton({
currentPage,
totalPages,
}: {
currentPage: number;
totalPages: number;
}) {
const router = useRouter();
const pathname = usePathname();
const [isLoading, setIsLoading] = useState(false);
const [page, setPage] = useState(currentPage);
const observerTarget = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting && page < totalPages && !isLoading) {
loadMore();
}
},
{ threshold: 1.0 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [page, totalPages, isLoading]);
function loadMore() {
setIsLoading(true);
const nextPage = page + 1;
router.push(`${pathname}?page=${nextPage}`, {
scroll: false,
});
setPage(nextPage);
setIsLoading(false);
}
return (
<div ref={observerTarget} className="load-more">
{isLoading && <p>Loading more images...</p>}
</div>
);
}---
Example 5: Real-time Chat App
Chat application with Server Actions for message sending and polling for updates.
Chat Room Page
// app/chat/[roomId]/page.tsx
import { getMessages } from "@/lib/chat";
import { ChatWindow } from "@/components/ChatWindow";
import { MessageInput } from "@/components/MessageInput";
export default async function ChatRoomPage({
params,
}: {
params: Promise<{ roomId: string }>;
}) {
const { roomId } = await params;
const messages = await getMessages(roomId);
return (
<div className="chat-room">
<ChatWindow roomId={roomId} initialMessages={messages} />
<MessageInput roomId={roomId} />
</div>
);
}Send Message Server Action
// app/actions/chat.ts
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const MessageSchema = z.object({
roomId: z.string().uuid(),
content: z.string().min(1).max(1000),
userId: z.string().uuid(),
});
export async function sendMessage(prevState: any, formData: FormData) {
const rawData = {
roomId: formData.get("roomId"),
content: formData.get("content"),
userId: formData.get("userId"),
};
const result = MessageSchema.safeParse(rawData);
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
};
}
try {
const message = await db.message.create({
data: result.data,
});
revalidatePath(`/chat/${result.data.roomId}`);
return {
success: true,
message,
};
} catch (error) {
return {
success: false,
message: "Failed to send message",
};
}
}---
See Also
Metadata API
Static Metadata
// app/layout.tsx o app/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "My App",
description: "A great application built with Next.js",
openGraph: {
title: "My App",
description: "A great application",
type: "website",
url: "https://myapp.com",
images: [
{
url: "https://myapp.com/og.png",
width: 1200,
height: 630,
alt: "My App",
},
],
},
twitter: {
card: "summary_large_image",
title: "My App",
description: "A great application",
images: ["https://myapp.com/twitter.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
icons: {
icon: "/favicon.ico",
shortcut: "/shortcut-icon.png",
apple: "/apple-icon.png",
},
manifest: "/manifest.json",
alternates: {
canonical: "https://myapp.com",
languages: {
"en-US": "https://myapp.com/en",
"de-DE": "https://myapp.com/de",
},
},
};Dynamic Metadata (generateMetadata)
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from "next";
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata(
{ params }: PageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await fetchPost(slug);
// Access parent metadata
const previousImages = (await parent).openGraph?.images || [];
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage, ...previousImages],
type: "article",
publishedTime: post.publishedAt,
authors: [post.author.name],
},
};
}Metadata Template (layout)
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
template: "%s | My App",
default: "My App - Tagline here",
},
description: "Default description",
};// app/blog/page.tsx
import type { Metadata } from "next";
// Risultato: "Blog | My App"
export const metadata: Metadata = {
title: "Blog",
};Metadata per Segmento
// app/layout.tsx (root)
export const metadata: Metadata = {
title: "My App",
description: "Root description",
};// app/blog/layout.tsx
export const metadata: Metadata = {
title: "Blog",
description: "Blog posts and articles",
};// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: PageProps) {
const { slug } = await params;
return {
title: `Post: ${slug}`, // Combina con template del layout
};
}viewport Export (Separato)
// app/layout.tsx
import type { Metadata, Viewport } from "next";
export const metadata: Metadata = {
title: "My App",
// ...
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 5,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};File-Based Metadata
favicon.ico e icon.ico
Posizionare nella stessa carta del segmento:
app/
├── favicon.ico → /favicon.ico
├── icon.png → /icon.png
└── blog/
└── icon.png → /blog/icon.png (dynamic)Dynamic Icon/OG Image
// app/icon.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const size = {
width: 32,
height: 32,
};
export default function Icon() {
return new ImageResponse(
(
<div
style={{
fontSize: 24,
background: "black",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "white",
}}
>
A
</div>
),
size
);
}// app/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt = "About Acme";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function OGImage() {
return new ImageResponse(
(
<div
style={{
fontSize: 128,
background: "white",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
About Acme
</div>
),
size
);
}robots.txt
Static
Crea app/robots.txt:
User-Agent: *
Allow: /
Disallow: /admin/
Sitemap: https://myapp.com/sitemap.xmlDynamic
// app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "Googlebot",
allow: "/",
disallow: "/admin/",
},
{
userAgent: "*",
allow: "/",
disallow: "/private/",
},
],
sitemap: "https://myapp.com/sitemap.xml",
};
}sitemap.xml
Static
Crea app/sitemap.xml:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://myapp.com</loc>
<lastmod>2024-01-01</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
</urlset>Dynamic
// app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetchPosts();
const postUrls = posts.map((post) => ({
url: `https://myapp.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: "weekly" as const,
priority: 0.8,
}));
return [
{
url: "https://myapp.com",
lastModified: new Date(),
changeFrequency: "daily",
priority: 1,
},
...postUrls,
];
}Structured Data (JSON-LD)
// app/page.tsx
export default function Page() {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
name: "My Company",
url: "https://myapp.com",
logo: "https://myapp.com/logo.png",
sameAs: [
"https://twitter.com/mycompany",
"https://linkedin.com/company/mycompany",
],
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<main>{/* Content */}</main>
</>
);
}// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params;
const post = await fetchPost(slug);
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
"@type": "Person",
name: post.author.name,
},
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>{/* Content */}</article>
</>
);
}Next.js 16 Migration Guide
Async APIs (Breaking Change)
In Next.js 16, cookies(), headers(), draftMode() sono tutti async.
Before (Next.js 15)
import { cookies, headers } from "next/headers";
export default function Page() {
const cookieStore = cookies();
const session = cookieStore.get("session");
const headersList = headers();
const userAgent = headersList.get("user-agent");
// ...
}After (Next.js 16)
import { cookies, headers } from "next/headers";
export default async function Page() {
const cookieStore = await cookies();
const session = cookieStore.get("session");
const headersList = await headers();
const userAgent = headersList.get("user-agent");
// ...
}Async Params e SearchParams
// Before
export default function Page({
params,
searchParams,
}: {
params: { slug: string };
searchParams: { sort?: string };
}) {
const { slug } = params;
const { sort } = searchParams;
// ...
}
// After
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ sort?: string }>;
}) {
const { slug } = await params;
const { sort } = await searchParams;
// ...
}proxy.ts (Replacement for middleware.ts)
Next.js 16 introduce proxy.ts come nuovo boundary per la logica di routing avanzato, sostituendo gradualmente middleware.ts.
When to Use proxy.ts vs middleware.ts
| Use Case | Solution |
|---|---|
| Header rewriting, auth redirects | middleware.ts |
| Complex routing logic, A/B testing, feature flags | proxy.ts |
| Request/Response modification | middleware.ts |
| Dynamic route selection | proxy.ts |
proxy.ts Structure
// app/proxy.ts
import { proxy } from "next/proxy";
export default proxy({
// Route matching
routes: [
{
pattern: "/old-path/:slug",
destination: "/new-path/:slug",
permanent: true,
},
{
pattern: "/blog/:slug",
destination: "/articles/:slug",
},
],
// Conditional routing
async selectRoute(request) {
const { pathname } = request.nextUrl;
// A/B testing
const variant = request.cookies.get("ab-variant")?.value ?? "a";
if (pathname === "/landing") {
return variant === "a" ? "/landing/a" : "/landing/b";
}
// Feature flags
const featureEnabled = await checkFeatureFlag("new-dashboard");
if (pathname === "/dashboard" && featureEnabled) {
return "/dashboard/v2";
}
return null; // Continue with normal routing
},
});middleware.ts (Still Valid)
// middleware.ts (root of project)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const session = request.cookies.get("session")?.value;
// Auth protection
if (request.nextUrl.pathname.startsWith("/dashboard") && !session) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set("x-custom-header", "value");
return response;
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};Turbopack (Stable)
Next.js 16 ha Turbopack come dev server predefinito.
# Già attivo di default, ma puoi verificare con:
next dev --turbopack
# Per disabilitare (non consigliato):
next dev --no-turbopackConfiguration
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
resolveAlias: {
"@components": "./app/components",
},
resolveExtensions: [".mdx", ".tsx", ".ts", ".jsx", ".js"],
},
};
export default nextConfig;React Compiler (Automatic Memoization)
Next.js 16 include React Compiler per memoizzazione automatica.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default nextConfig;Manual Memo Still Works
// React Compiler gestisce automaticamente la maggior parte dei casi
// ma puoi ancora usare manualmente quando necessario
import { memo, useMemo, useCallback } from "react";
// Ancora valido per casi complessi
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
const processed = useMemo(() => heavyComputation(data), [data]);
// ...
});"use cache" Directive
La nuova direttiva per caching esplicito (sostituisce in parte fetch cache).
"use cache";
import { cacheLife, cacheTag } from "next/cache";
export default async function Page() {
cacheLife("hours");
cacheTag("homepage");
const data = await fetchData();
return <Component data={data} />;
}next.config.ts (TypeScript Default)
Next.js 16 consiglia .ts per la configurazione.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Configurazioni
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.example.com",
},
],
},
// Experimental
experimental: {
reactCompiler: true,
cacheLife: {
blog: {
stale: 3600,
revalidate: 900,
expire: 86400,
},
},
},
};
export default nextConfig;ESLint Configuration
# Installazione dipendenze aggiornate
npm install -D eslint-config-next@latest eslint@latest// eslint.config.mjs
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;Next.js App Router - Detailed Code Patterns
This document contains comprehensive code patterns for Next.js 16+ App Router development.
Table of Contents
- Server Components Patterns
- Client Components Patterns
- Server Actions Patterns
- Route Handlers Patterns
- Caching Patterns
- Routing Patterns
- Next.js 16 Async API Patterns
---
Server Components Patterns
Basic Data Fetching
Server Components are the default and ideal for data fetching.
// app/users/page.tsx
async function getUsers() {
const apiUrl = process.env.API_URL;
const res = await fetch(`${apiUrl}/users`, {
// Next.js extends fetch with caching options
next: { revalidate: 3600 }, // Cache for 1 hour
});
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<main>
<h1>Users</h1>
{users.map(user => <UserCard key={user.id} user={user} />)}
</main>
);
}Parallel Data Fetching
Fetch independent data in parallel for better performance.
async function getProduct(id: string) {
const res = await fetch(`${API_URL}/products/${id}`, {
next: { revalidate: 3600 },
});
return res.json();
}
async function getReviews(productId: string) {
const res = await fetch(`${API_URL}/reviews?product=${productId}`, {
next: { revalidate: 60 }, // Shorter cache for reviews
});
return res.json();
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Parallel fetching with Promise.all
const [product, reviews] = await Promise.all([
getProduct(id),
getReviews(id),
]);
return (
<main>
<ProductDetail product={product} />
<ReviewsList reviews={reviews} />
</main>
);
}Using React's cache() for Deduplication
Prevent duplicate fetches within the same component tree.
import { cache } from "react";
const getSingleProduct = cache(async (id: string) => {
const res = await fetch(`${API_URL}/products/${id}`);
return res.json();
});
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// These will dedupe - only one fetch
const product = await getSingleProduct(id);
const sameProduct = await getSingleProduct(id);
return <ProductDetail product={product} />;
}---
Client Components Patterns
Using React Hooks
"use client";
import { useState, useEffect } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
// Only runs on client
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle Visibility
</button>
{isVisible && <p>Visible content</p>}
</div>
);
}Browser APIs Access
"use client";
import { useState, useEffect } from "react";
export default function Geolocation() {
const [location, setLocation] = useState<{
lat: number;
lng: number;
} | null>(null);
useEffect(() => {
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(
position => {
setLocation({
lat: position.coords.latitude,
lng: position.coords.longitude,
});
},
error => {
console.error("Geolocation error:", error);
}
);
}
}, []);
if (!location) return <p>Loading location...</p>;
return (
<p>
Latitude: {location.lat}, Longitude: {location.lng}
</p>
);
}Combining Server and Client Components
Pass server-fetched data as props to Client Components.
// Server Component
async function getProduct(id: string) {
const res = await fetch(`${API_URL}/products/${id}`);
return res.json();
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProduct(id);
return <ProductClient product={product} />;
}
// Client Component
"use client";
import { useState } from "react";
export function ProductClient({ product }: { product: Product }) {
const [isLiked, setIsLiked] = useState(false);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<button onClick={() => setIsLiked(!isLiked)}>
{isLiked ? "♥ Liked" : "♡ Like"}
</button>
</div>
);
}---
Server Actions Patterns
Basic Server Action
Define actions in separate files with "use server" directive.
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { db } from "./db";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const user = await db.user.create({
data: { name, email },
});
revalidatePath("/users");
return user;
}Server Action with Validation
// app/actions/users.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
const CreateUserSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
role: z.enum(["user", "admin"]).default("user"),
});
export async function createUser(prevState: any, formData: FormData) {
const rawData = {
name: formData.get("name"),
email: formData.get("email"),
role: formData.get("role"),
};
const result = CreateUserSchema.safeParse(rawData);
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors,
message: "Validation failed",
};
}
try {
const user = await db.user.create({
data: result.data,
});
revalidatePath("/users");
return {
success: true,
user,
message: "User created successfully",
};
} catch (error) {
return {
message: "Failed to create user",
errors: { _form: ["Database error occurred"] },
};
}
}Optimistic Updates with useOptimistic
"use client";
import { useOptimistic, useActionState } from "react";
import { likePost } from "./actions";
export function LikeButton({ postId, initialLikes }: {
postId: string;
initialLikes: number;
}) {
const [state, formAction] = useActionState(likePost, null);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, newLikes) => state + 1
);
async function handleSubmit() {
addOptimisticLike(1);
formAction(new FormData());
}
return (
<form action={handleSubmit}>
<button type="submit">
♥ {optimisticLikes} Likes
</button>
</form>
);
}Server Actions with Error Handling
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
export async function deleteUser(formData: FormData) {
const id = formData.get("id") as string;
if (!id) {
return {
success: false,
error: "User ID is required",
};
}
try {
await db.user.delete({ where: { id } });
revalidatePath("/users");
return {
success: true,
message: "User deleted successfully",
};
} catch (error) {
if (error instanceof Error) {
return {
success: false,
error: error.message,
};
}
return {
success: false,
error: "An unknown error occurred",
};
}
}---
Route Handlers Patterns
Basic CRUD Endpoints
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
// GET all users
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "10");
const users = await db.user.findMany({
skip: (page - 1) * limit,
take: limit,
});
return NextResponse.json({
users,
page,
limit,
total: await db.user.count(),
});
}
// POST create user
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ errors: result.error.flatten().fieldErrors },
{ status: 400 }
);
}
const user = await db.user.create({
data: result.data,
});
return NextResponse.json(user, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}Dynamic Route Handlers
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
interface RouteParams {
params: Promise<{ id: string }>;
}
// GET single user
export async function GET(
request: NextRequest,
{ params }: RouteParams
) {
const { id } = await params;
const user = await db.user.findUnique({ where: { id } });
if (!user) {
return NextResponse.json(
{ error: "User not found" },
{ status: 404 }
);
}
return NextResponse.json(user);
}
// PATCH update user
export async function PATCH(
request: NextRequest,
{ params }: RouteParams
) {
const { id } = await params;
const body = await request.json();
try {
const user = await db.user.update({
where: { id },
data: body,
});
return NextResponse.json(user);
} catch (error) {
return NextResponse.json(
{ error: "Failed to update user" },
{ status: 500 }
);
}
}
// DELETE user
export async function DELETE(
request: NextRequest,
{ params }: RouteParams
) {
const { id } = await params;
try {
await db.user.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
} catch (error) {
return NextResponse.json(
{ error: "Failed to delete user" },
{ status: 500 }
);
}
}Route Handler with Authentication
// app/api/protected/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export async function GET(request: NextRequest) {
const session = await auth();
if (!session) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// Process authenticated request
const data = await getProtectedData(session.user.id);
return NextResponse.json(data);
}---
Caching Patterns
Basic "use cache" Directive
Next.js 16+ introduces explicit caching with the "use cache" directive.
"use cache";
import { cacheLife, cacheTag } from "next/cache";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Tag for selective revalidation
cacheTag(`product-${id}`, "products");
// Set cache duration
cacheLife("hours");
const product = await fetchProduct(id);
return <ProductDetail product={product} />;
}Cache Profiles with cacheLife
"use cache";
import { cacheLife } from "next/cache";
// Predefined cache profiles
cacheLife("minutes"); // 1 minute
cacheLife("hours"); // 1 hour
cacheLife("days"); // 1 day
cacheLife("max"); // Maximum caching (1 year)
// Custom duration
cacheLife({
stale: 60, // Serve stale for 60s while revalidating
revalidate: 3600, // Revalidate every hour
});Tag-Based Revalidation
"use cache";
import { cacheTag } from "next/cache";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Multiple tags for flexible revalidation
cacheTag(`product-${id}`, "products", "catalog");
const product = await fetchProduct(id);
return <ProductDetail product={product} />;
}
// Revalidate on demand
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { tag } = await request.json();
revalidateTag(tag);
return NextResponse.json({ revalidated: true });
}On-Demand Revalidation
// Server Action that triggers revalidation
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProduct(productId: string, data: any) {
await db.product.update({
where: { id: productId },
data,
});
// Revalidate specific product
revalidateTag(`product-${productId}`);
// Or revalidate all products
revalidateTag("products");
// Or revalidate by path
revalidatePath(`/products/${productId}`);
return { success: true };
}---
Routing Patterns
Parallel Routes
Create multiple independent route slots using @folder convention.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div className="dashboard">
<header>Dashboard Header</header>
<div className="content">
<main>{children}</main>
<aside className="panels">
<div className="team-panel">{team}</div>
<div className="analytics-panel">{analytics}</div>
</aside>
</div>
</div>
);
}
// app/dashboard/@team/page.tsx
export default function TeamPage() {
return <div>Team Section</div>;
}
// app/dashboard/@analytics/page.tsx
export default async function AnalyticsPage() {
const stats = await getAnalytics();
return <div>Analytics: {stats.views} views</div>;
}Intercepting Routes
Show a modal while preserving the underlying page context.
// app/photos/[id]/page.tsx
export default function PhotoPage({ params }: {
params: Promise<{ id: string }>;
}) {
return <PhotoDetail id={(await params).id} />;
}
// app/(.)photos/[id]/page.tsx - Intercepted route
export default function PhotoModal({ params }: {
params: Promise<{ id: string }>;
}) {
return <PhotoModal id={(await params).id} />;
}
// PhotoModal component
"use client";
import { useRouter } from "next/navigation";
export function PhotoModal({ id }: { id: string }) {
const router = useRouter();
return (
<div className="modal-overlay" onClick={() => router.back()}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<img src={`/photos/${id}.jpg`} alt={`Photo ${id}`} />
<button onClick={() => router.back()}>Close</button>
</div>
</div>
);
}Route Groups
Organize routes without affecting URL structure.
// app/(marketing)/about/page.tsx -> /about
// app/(marketing)/contact/page.tsx -> /contact
// app/(dashboard)/profile/page.tsx -> /profile
// Shared layout for marketing routes
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="marketing">
<Navigation />
{children}
<Footer />
</div>
);
}
// Different layout for dashboard routes
// app/(dashboard)/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}---
Next.js 16 Async API Patterns
Async cookies(), headers(), draftMode()
All Next.js APIs are async in version 16.
import { cookies, headers, draftMode } from "next/headers";
export default async function Page() {
const cookieStore = await cookies();
const headersList = await headers();
const { isEnabled } = await draftMode();
const session = cookieStore.get("session")?.value;
const userAgent = headersList.get("user-agent");
return (
<div>
<p>Session: {session}</p>
<p>User Agent: {userAgent}</p>
<p>Draft Mode: {isEnabled ? "Enabled" : "Disabled"}</p>
</div>
);
}Async params and searchParams
Route parameters are Promise-based in Next.js 16.
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ sort?: string; filter?: string }>;
}) {
const { slug } = await params;
const { sort, filter } = await searchParams;
const data = await fetchData({ slug, sort, filter });
return <PageContent data={data} />;
}Streaming with Suspense
Leverage async APIs for progressive rendering.
import { Suspense } from "react";
export default async function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
);
}
async function Stats() {
const stats = await getStats();
return <StatsDisplay stats={stats} />;
}
async function RecentActivity() {
const activities = await getRecentActivity();
return <ActivityList activities={activities} />;
}---
See Also
Routing Patterns
File Conventions
| File | Descrizione |
|---|---|
page.tsx | Pagina pubblica accessibile tramite URL |
layout.tsx | Layout condiviso che wrappa i page |
loading.tsx | UI di caricamento durante fetch dati |
error.tsx | UI per gestione errori |
not-found.tsx | UI per 404 |
template.tsx | Layout re-mounted su navigazione |
default.tsx | Fallback per parallel routes |
route.ts | API Route Handler |
Parallel Routes (@slot)
Permettono di renderizzare più pagine nello stesso layout simultaneamente.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid grid-cols-2">
{team}
{analytics}
</div>
</div>
);
}// app/dashboard/@team/page.tsx
export default function TeamPage() {
return <div>Team Section</div>;
}// app/dashboard/@analytics/page.tsx
export default function AnalyticsPage() {
return <div>Analytics Section</div>;
}Intercepting Routes
Permettono di intercettare route e mostrarle in modalità diversa (es: modal).
| Pattern | Intercetta |
|---|---|
(.) | Stesso livello |
(..) | Un livello sopra |
(..)(..) | Due livelli sopra |
(...) | Root |
app/
├── feed/
│ └── page.tsx
└── feed/
└── @modal/
└── (.)photo/
└── [id]/
└── page.tsx <- Intercetta /feed/photo/[id]Route Groups
Organizzano route senza influenzare l'URL (usando parentesi).
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx -> /about
│ └── contact/
│ └── page.tsx -> /contact
├── (shop)/
│ ├── products/
│ │ └── page.tsx -> /products
│ └── cart/
│ └── page.tsx -> /cart
└── layout.tsxDynamic Routes
// app/blog/[slug]/page.tsx
interface PageProps {
params: Promise<{ slug: string }>;
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params;
// Use slug...
}
// Catch-all
// app/docs/[...slug]/page.tsx
// Optional catch-all
// app/docs/[[...slug]]/page.tsxGenerate Static Params
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetchPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Con multiple dynamic segments
export async function generateStaticParams() {
const products = await fetchProducts();
return products.map((product) => ({
category: product.category,
id: product.id,
}));
}Server Actions
Basic Server Action
// app/actions.ts
"use server";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
await db.user.create({
data: { name, email },
});
revalidatePath("/users");
}// app/users/page.tsx
import { createUser } from "./actions";
export default function UserForm() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<button type="submit">Create</button>
</form>
);
}Server Action with Zod Validation
// app/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const createUserSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
});
export async function createUser(formData: FormData) {
const validated = createUserSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!validated.success) {
return {
error: validated.error.flatten().fieldErrors,
};
}
try {
await db.user.create({
data: validated.data,
});
revalidatePath("/users");
return { success: true };
} catch (error) {
return { error: "Failed to create user" };
}
}useActionState Hook (React 19)
"use client";
import { useActionState } from "react";
import { createUser } from "./actions";
const initialState = {
error: null as Record<string, string[]> | null,
success: false,
};
export default function UserForm() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<input name="name" placeholder="Name" />
{state.error?.name && <span>{state.error.name[0]}</span>}
<input name="email" type="email" placeholder="Email" />
{state.error?.email && <span>{state.error.email[0]}</span>}
<button type="submit" disabled={pending}>
{pending ? "Creating..." : "Create"}
</button>
{state.success && <p>User created!</p>}
</form>
);
}useFormStatus Hook
"use client";
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
export default function Form() {
return (
<form action={handleSubmit}>
<input name="title" />
<SubmitButton />
</form>
);
}Server Action with bind (arguments extra)
// app/actions.ts
"use server";
export async function updateUser(userId: string, formData: FormData) {
// userId is passed via bind
const name = formData.get("name");
await db.user.update({
where: { id: userId },
data: { name },
});
revalidatePath("/users");
}// app/users/[id]/page.tsx
import { updateUser } from "./actions";
export default async function EditUser({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const updateUserWithId = updateUser.bind(null, id);
return (
<form action={updateUserWithId}>
<input name="name" placeholder="New name" />
<button type="submit">Update</button>
</form>
);
}Optimistic Updates
"use client";
import { useOptimistic } from "react";
import { sendMessage } from "./actions";
interface Message {
id: string;
text: string;
sending?: boolean;
}
export default function Chat({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: string) => [
...state,
{ id: Math.random().toString(), text: newMessage, sending: true },
]
);
async function handleSubmit(formData: FormData) {
const text = formData.get("message") as string;
addOptimisticMessage(text);
await sendMessage(text);
}
return (
<div>
{optimisticMessages.map((msg) => (
<div key={msg.id} style={{ opacity: msg.sending ? 0.5 : 1 }}>
{msg.text}
</div>
))}
<form action={handleSubmit}>
<input name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}Error Handling
"use server";
import { redirect } from "next/navigation";
export async function deleteUser(userId: string) {
try {
await db.user.delete({ where: { id: userId } });
revalidatePath("/users");
redirect("/users");
} catch (error) {
throw new Error("Failed to delete user");
}
}Cookies and Headers in Server Actions
"use server";
import { cookies, headers } from "next/headers";
export async function trackEvent(event: string) {
const cookieStore = await cookies();
const headersList = await headers();
const sessionId = cookieStore.get("session-id")?.value;
const userAgent = headersList.get("user-agent");
await analytics.track({
event,
sessionId,
userAgent,
});
}Related skills
Forks & variants (1)
Nextjs App Router has 1 known copy in the catalog totaling 22 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 22 installs
How it compares
Pick nextjs-app-router for App Router rendering boundary decisions; use Next.js data-fetching docs when you only need caching and revalidation specifics.
FAQ
What does nextjs-app-router do?
Provides patterns and code examples for building Next.js 16+ applications with App Router architecture. Use when creating projects with App Router, implementing Server Components and Client Components
When should I use nextjs-app-router?
During build backend work for backend & apis.
Is nextjs-app-router safe to install?
Review the Security Audits panel on this listing before production use.