From tdd-dev-workflow
This skill should be used when the user asks about "Next.js", "App Router", "Server Components", "Client Components", "route handlers", "server actions", "streaming", "parallel routes", "intercepting routes", "ISR", "SSG", "SSR", "Next.js middleware", "Next.js caching", "Next.js metadata", or needs guidance on Next.js 14/15+ architecture and patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tdd-dev-workflow:nextjs-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
| Skill | Reason |
| Skill | Reason |
|---|---|
react-best-practices | Next.js extends React component patterns, hooks, and rendering model |
tailwind-patterns | Default styling approach in Next.js projects; co-located with components |
| Skill | Reason |
|---|---|
nextjs-supabase-auth | Builds authentication flows on top of Next.js routing and server patterns |
Default to Server Components. Never add 'use client' without a specific reason.
useState, useReducer, or useEffect is required.onClick, onChange, onSubmit) are needed.window, localStorage, navigator) are accessed.'use client' boundary as low as possibleExtract the interactive part into a small Client Component. Keep the parent as a Server Component. Never mark an entire page as 'use client'.
// GOOD: Only the button is a Client Component
// page.tsx (Server Component)
import { LikeButton } from './like-button';
export default async function Page() {
const data = await fetchData();
return <article>{data.content}<LikeButton id={data.id} /></article>;
}
// like-button.tsx (Client Component)
'use client';
export function LikeButton({ id }: { id: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(true)}>{liked ? 'Liked' : 'Like'}</button>;
}
| File | Purpose |
|---|---|
page.tsx | Route UI, makes segment publicly accessible |
layout.tsx | Shared UI wrapping children, preserved across navigations |
loading.tsx | Instant loading state via Suspense boundary |
error.tsx | Error boundary for the segment (must be 'use client') |
not-found.tsx | UI for notFound() calls |
template.tsx | Like layout but remounts on navigation |
route.ts | API endpoint (GET, POST, PUT, DELETE handlers) |
default.tsx | Fallback for parallel route slots |
Organize routes without affecting the URL path.
app/
(marketing)/
about/page.tsx -> /about
blog/page.tsx -> /blog
(dashboard)/
settings/page.tsx -> /settings
profile/page.tsx -> /profile
Render multiple pages simultaneously in the same layout using named slots.
app/
@modal/
default.tsx
login/page.tsx
@sidebar/
default.tsx
page.tsx
layout.tsx -> receives { children, modal, sidebar }
page.tsx
Define the layout to accept slot props:
export default function Layout({
children,
modal,
sidebar,
}: {
children: React.ReactNode;
modal: React.ReactNode;
sidebar: React.ReactNode;
}) {
return (
<div className="flex">
<aside>{sidebar}</aside>
<main>{children}</main>
{modal}
</div>
);
}
Intercept a route to show it in a different context (e.g., modal) while preserving the full-page URL for direct navigation and sharing.
| Convention | Matches |
|---|---|
(.) | Same level |
(..) | One level up |
(..)(..) | Two levels up |
(...) | Root |
// app/posts/page.tsx -- Server Component by default
export default async function PostsPage() {
const posts = await db.post.findMany();
return <PostList posts={posts} />;
}
Default behavior. Pages are cached at build time.
// Statically generated at build
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{data}</div>;
}
// Revalidate every hour
export const revalidate = 3600;
export default async function Page() {
const data = await fetch('https://api.example.com/data',
{ next: { revalidate: 3600 } }
);
return <div>{data}</div>;
}
// Force dynamic rendering
export const dynamic = 'force-dynamic';
// Or implicitly dynamic via cookies/headers
import { cookies } from 'next/headers';
export default async function Page() {
const token = cookies().get('session');
// ...
}
Progressively render UI as data becomes available.
import { Suspense } from 'react';
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<CardSkeleton />}>
<SlowDataComponent />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<AnotherSlowComponent />
</Suspense>
</main>
);
}
Mark functions with 'use server' for secure server-side mutations callable from Client Components.
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
revalidatePath('/posts');
}
// Works without JavaScript enabled
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}
revalidatePath('/posts') -- Invalidate a specific path.revalidateTag('posts') -- Invalidate all fetches tagged with 'posts'.Next.js has three caching layers. Understand each to avoid stale data bugs.
| Layer | What | Duration | Opt Out |
|---|---|---|---|
| Request Memoization | Deduplicates identical fetch calls in a single render | Per request | N/A (automatic) |
| Data Cache | Persists fetch results across requests | Indefinite | { cache: 'no-store' } |
| Full Route Cache | Caches rendered HTML and RSC payload | Until revalidation | dynamic = 'force-dynamic' |
Use unstable_cache for caching non-fetch data (database queries, computations).
import { unstable_cache } from 'next/cache';
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
['user-cache'],
{ revalidate: 3600, tags: ['users'] }
);
Place middleware.ts at the project root (same level as app/).
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
Keep middleware lightweight. It runs on every matched request at the edge. No heavy computation, no database calls.
next/image with width, height, and alt. Automatic lazy loading, format optimization, and responsive sizing.next/font/google or next/font/local. Eliminates layout shift, self-hosts fonts.next/dynamic for heavy components not needed on initial load.@next/bundle-analyzer to identify large dependencies.metadata object or generateMetadata function for SEO.import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Page Title',
description: 'Page description for SEO',
openGraph: { title: 'OG Title', description: 'OG Description' },
};
Define API endpoints alongside pages using the route.ts convention. Route handlers support standard HTTP methods.
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = Number(searchParams.get('page') ?? '1');
const posts = await db.post.findMany({
skip: (page - 1) * 10,
take: 10,
});
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await db.post.create({ data: body });
return NextResponse.json(post, { status: 201 });
}
Route handlers are cached by default when using the GET method with no Request object. Add export const dynamic = 'force-dynamic' to opt out of caching.
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await db.post.findUnique({ where: { id: params.id } });
if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(post);
}
Prefer Server Actions for mutations triggered by forms and Client Components. Use route handlers for webhooks, third-party integrations, and endpoints consumed by external clients.
// app/about/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn about our company and mission.',
robots: { index: true, follow: true },
};
// app/posts/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await db.post.findUnique({ where: { slug: params.slug } });
return {
title: post?.title ?? 'Post Not Found',
description: post?.excerpt,
openGraph: {
title: post?.title,
description: post?.excerpt,
images: post?.coverImage ? [{ url: post.coverImage }] : [],
},
};
}
Generate sitemap.xml and robots.txt using the sitemap.ts and robots.ts file conventions in the app/ directory.
| Anti-Pattern | Why It Is Wrong | Correct Approach |
|---|---|---|
'use client' at page level | Opts entire page out of server rendering | Extract only interactive parts into Client Components |
useEffect for data fetching | Causes waterfall, no SSR, loading flicker | Fetch in Server Components directly |
Missing loading.tsx | No instant feedback during navigation | Add loading.tsx to every dynamic route segment |
Missing error.tsx | Unhandled errors crash the entire page | Add error.tsx boundaries at appropriate levels |
Ignoring revalidate | Stale data served indefinitely | Set explicit revalidation strategy |
| Heavy middleware logic | Slows every request, runs at edge | Keep middleware to auth checks, redirects, headers only |
| Route handlers for form mutations | Unnecessary indirection when Server Actions work | Use Server Actions for form handling and data mutations |
Not using generateStaticParams | Dynamic pages miss static optimization at build time | Generate params for known dynamic routes |
For detailed implementation examples (streaming, parallel routes, intercepting routes, server actions with forms, middleware auth), read references/implementation-playbook.md.
npx claudepluginhub inteligentsensingsolutions/tdd-dev-workflow --plugin tdd-dev-workflowGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.