nextjs-app-router
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
- 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 Conventions
| File | Purpose |
|---|---|
page.tsx |
Route page component |
layout.tsx |
Shared layout wrapper |
loading.tsx |
Suspense loading UI |
error.tsx |
Error boundary |
not-found.tsx |
404 page |
template.tsx |
Re-mounting layout |
route.ts |
API Route Handler |
default.tsx |
Parallel route fallback |
proxy.ts |
Routing boundary (Next.js 16) |
Directives
| Directive | Purpose |
|---|---|
"use server" |
Mark Server Action functions |
"use client" |
Mark Client Component boundary |
"use cache" |
Enable explicit caching (Next.js 16) |
Instructions
Create New Project
npx create-next-app@latest my-app --typescript --tailwind --app --turbopack
Implement Server Component
Server Components are the default in App Router.
// 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>
);
}
Implement 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>
);
}
Create 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 validation with Zod, 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 caching patterns.
Create Route Handler
Implement API endpoints using Route Handlers.
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const users = await db.user.findMany();
return NextResponse.json(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 });
}
Dynamic segments use [param]:
// app/api/users/[id]/route.ts
interface RouteParams {
params: Promise<{ id: string }>;
}
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);
}
Handle Next.js 16 Async APIs
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>...</div>;
}
Params and searchParams are also async:
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.
Implement Parallel Routes
Use @folder convention for parallel route slots.
// 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>;
}
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, useContext)
- Browser APIs (window, document, localStorage)
- Event handlers (onClick, onSubmit)
- 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
Caching Strategy
"use cache";
import { cacheLife, cacheTag } from "next/cache";
// Set cache duration
cacheLife("hours");
// Tag for revalidation
cacheTag("resource-name");
Performance Checklist
- Use
loading.tsxfor Suspense boundaries - Use
next/imagefor optimized images - Use
next/fontfor font optimization - Enable React Compiler in
next.config.ts - Add
error.tsxfor error handling - Add
not-found.tsxfor 404 handling
Examples
Example 1: Create Blog Post with Server Action
Input: Create a form to submit blog posts with 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: Product Page with Caching
Input: Create a cached product page with 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>
<p>${product.price}</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 main content areas
Output:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
sidebar,
stats,
}: {
children: React.ReactNode;
sidebar: React.ReactNode;
stats: 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>
<div>Revenue: {stats.revenue}</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
- Attempting to use
awaitin a Client Component will cause a build error - Accessing
windowordocumentin Server Components will throw an error - Forgetting to await
cookies()orheaders()in Next.js 16 will result in a Promise instead of the actual values - Server Actions without proper validation can expose your database to unauthorized access
- External Data Fetching: Server Components that fetch data from external APIs (
fetch()calls to third-party URLs) process untrusted content; always validate, sanitize, and type-check fetched responses before rendering, and use environment variables for API URLs rather than hardcoding them
References
Consult these files for detailed patterns:
- references/app-router-fundamentals.md - Server/Client Components, file conventions, navigation, next/image, next/font
- references/routing-patterns.md - Parallel routes, intercepting routes, route groups, dynamic routes
- references/caching-strategies.md - "use cache", cacheLife, cacheTag, revalidation
- references/server-actions.md - Server Actions, useActionState, validation, optimistic updates
- references/nextjs16-migration.md - Async APIs, proxy.ts, Turbopack, config
- references/data-fetching.md - Data patterns, Suspense, streaming
- references/metadata-api.md - generateMetadata, OpenGraph, sitemap