Next.js 16 App Router patterns including server components, client components, server actions, route handlers, layouts, metadata API, dynamic routes, file conventions, data fetching, caching...
Activate this skill when working on:
Use Server Components (default) when:
Use Client Components ('use client') when:
useState, useEffect, useContext)Example:
// app/board/[id]/page.tsx - Server Component (default)
import { getBoard } from "@/lib/actions/board/getBoard";
export default async function BoardPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const board = await getBoard(id);
return <BoardView board={board} />;
}
// components/board/BoardView.tsx - Client Component
"use client";
import { useState } from "react";
import { PostProvider } from "@/components/board/PostProvider";
export function BoardView({ board }) {
const [filter, setFilter] = useState("all");
return <PostProvider boardId={board.id}>{/* Interactive UI */}</PostProvider>;
}
CRITICAL: All server actions MUST use actionWithAuth or rbacWithAuth wrappers (see rbac-security skill).
Pattern:
// lib/actions/post/createPost.ts
"use server";
import { rbacWithAuth } from "@/lib/actions/actionWithAuth";
import { db } from "@/db";
import { postTable } from "@/db/schema";
export const createPost = async (
boardId: string,
content: string,
type: PostType
) =>
rbacWithAuth(boardId, async (userId) => {
const post = await db
.insert(postTable)
.values({
id: nanoid(),
boardId,
userId,
content,
type,
createdAt: new Date(),
})
.returning();
return post[0];
});
Usage in Client Components:
"use client";
import { createPost } from "@/lib/actions/post/createPost";
import { useTransition } from "react";
export function CreatePostForm({ boardId }) {
const [isPending, startTransition] = useTransition();
const handleSubmit = async (formData: FormData) => {
startTransition(async () => {
await createPost(boardId, formData.get("content") as string, "went_well");
});
};
return <form action={handleSubmit}>...</form>;
}
Special Files:
page.tsx - Unique UI for a routelayout.tsx - Shared UI for segments and childrenloading.tsx - Loading UI (Suspense boundary)error.tsx - Error UI (Error boundary)not-found.tsx - Not found UIroute.ts - API endpoint (Route Handler)Route Organization:
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── board/
│ ├── layout.tsx # Board layout
│ ├── page.tsx # Board list
│ └── [id]/
│ ├── page.tsx # Board detail (dynamic route)
│ ├── loading.tsx # Loading state
│ └── error.tsx # Error handling
└── api/
└── webhooks/
└── route.ts # API route handler
Static Metadata:
// app/board/[id]/page.tsx
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Board Details",
description: "View and manage your retrospective board",
};
Dynamic Metadata:
// app/board/[id]/page.tsx
import { getBoard } from "@/lib/actions/board/getBoard";
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}): Promise<Metadata> {
const { id } = await params;
const board = await getBoard(id);
return {
title: `${board.name} - Ree Board`,
description: board.description,
openGraph: {
title: board.name,
description: board.description,
},
};
}
Lazy Load Heavy Components:
import dynamic from "next/dynamic";
// Drag-and-drop is lazy loaded in the project
const PostDragDrop = dynamic(() => import("@/components/board/PostDragDrop"), {
ssr: false,
loading: () => <LoadingSkeleton />,
});
Bad:
"use client"; // ❌ Unnecessary - no hooks or interactivity
export default function Page() {
return <div>Static content</div>;
}
Good:
// ✅ Server component by default
export default function Page() {
return <div>Static content</div>;
}
Bad:
"use client";
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/data")
.then((r) => r.json())
.then(setData); // ❌
}, []);
}
Good:
// ✅ Server component fetches data
export default async function Page() {
const data = await getData();
return <ClientView data={data} />;
}
Bad:
"use server";
export async function deleteBoard(id: string) {
// ❌ No auth check - security vulnerability
await db.delete(boardTable).where(eq(boardTable.id, id));
}
Good:
"use server";
export const deleteBoard = async (id: string) =>
rbacWithAuth(id, async (userId) => {
// ✅ Authentication and RBAC enforced
await db.delete(boardTable).where(eq(boardTable.id, id));
});
Bad:
export default async function Page() {
const data = await slowDataFetch(); // ❌ Blocks entire page
return <div>{data}</div>;
}
Good:
import { Suspense } from "react";
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
);
}
async function DataComponent() {
const data = await slowDataFetch();
return <div>{data}</div>;
}
app/ - Next.js App Router directoryapp/layout.tsx - Root layout with providersapp/board/[id]/page.tsx - Dynamic board pagelib/actions/ - Server actions by domainproxy.ts - Supabase authentication proxy (Next.js 16)next.config.js - Next.js configurationlib/actions/[entity]/actionWithAuth or rbacWithAuthLast Updated: 2026-01-10