name: Next.js 16+ Conventions description: This skill should be used when working with Next.js 16+ codebases, when the user asks about "Next.js routing", "App Router", "Server Components", "Client Components", "Server Actions", "Next.js API routes", "Next.js layouts", or when EREN detects a Next.js project. Provides modern Next.js 16+ patterns and best practices. version: 0.1.0
Next.js 16+ Conventions
Next.js 16+ builds upon the App Router paradigm with enhanced Server Components, improved caching, and streamlined data fetching. This skill provides decision trees and patterns for working with Next.js 16+ codebases effectively.
Core Principles
Next.js 16+ emphasizes:
- Server Components as the default (zero client JS)
- App Router with file-based routing conventions
- Server Actions for mutations and form handling
- Streaming and Suspense for progressive loading
- Built-in caching with fine-grained control
Server vs Client Components
Decision Tree: Component Type
- Does it use browser APIs (window, localStorage)? → Client Component
- Does it use React hooks (useState, useEffect)? → Client Component
- Does it need event handlers (onClick)? → Client Component
- Does it fetch data at render time? → Server Component
- Does it access backend resources directly? → Server Component
- Is it purely presentational? → Server Component (default)
Server Component (Default)
// app/users/page.tsx - Server Component (no directive needed)
import { db } from '@/lib/db';
export default async function UsersPage() {
const users = await db.user.findMany();
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Client Component
// app/components/counter.tsx - Client Component
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
Composition Pattern
Server Components can import Client Components, not vice versa:
// app/dashboard/page.tsx - Server Component
import { db } from '@/lib/db';
import { InteractiveChart } from './interactive-chart'; // Client Component
export default async function DashboardPage() {
const data = await db.analytics.getChartData();
return (
<div>
<h1>Dashboard</h1>
{/* Pass server data to client component */}
<InteractiveChart data={data} />
</div>
);
}
App Router File Conventions
Route Files
| File | Purpose |
|---|---|
page.tsx |
Route UI (required for route to be accessible) |
layout.tsx |
Shared UI wrapper (persists across navigation) |
loading.tsx |
Loading UI (Suspense boundary) |
error.tsx |
Error UI (Error boundary) |
not-found.tsx |
404 UI |
route.ts |
API endpoint |
Directory Structure
app/
├── layout.tsx # Root layout
├── page.tsx # Home page (/)
├── globals.css
├── (auth)/ # Route group (no URL impact)
│ ├── login/
│ │ └── page.tsx # /login
│ └── register/
│ └── page.tsx # /register
├── dashboard/
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx # /dashboard
│ ├── loading.tsx # Loading state
│ └── [teamId]/ # Dynamic segment
│ ├── page.tsx # /dashboard/[teamId]
│ └── settings/
│ └── page.tsx # /dashboard/[teamId]/settings
└── api/
└── users/
└── route.ts # API: /api/users
Decision Tree: Route Organization
- Shared UI across routes? → Create layout.tsx in parent
- Routes with shared logic but different URLs? → Use route groups
(folder) - Dynamic path segment? → Use
[param]folder - Catch-all segments? → Use
[...slug]or[[...slug]] - API endpoint? → Create route.ts (not page.tsx)
Server Actions
Server Actions handle mutations directly from components.
Form Action Pattern
// app/users/actions.ts
'use server';
import { db } from '@/lib/db';
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');
}
// app/users/create/page.tsx - Server Component
import { createUser } from '../actions';
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Create User</button>
</form>
);
}
Client-Side Server Action
// app/components/delete-button.tsx
'use client';
import { deleteUser } from '../actions';
import { useTransition } from 'react';
export function DeleteButton({ userId }: { userId: string }) {
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => startTransition(() => deleteUser(userId))}
>
{isPending ? 'Deleting...' : 'Delete'}
</button>
);
}
Decision Tree: Server Actions
- Simple form submission? → Use action attribute directly
- Need loading state? → Use useTransition in Client Component
- Need form validation? → Use useActionState hook
- Need optimistic updates? → Use useOptimistic hook
- Revalidate after mutation? → Call revalidatePath or revalidateTag
Data Fetching
Fetch in Server Components
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return <PostList posts={posts} />;
}
Caching Options
| Option | Effect |
|---|---|
{ cache: 'force-cache' } |
Cache indefinitely (default) |
{ cache: 'no-store' } |
Never cache |
{ next: { revalidate: N } } |
Revalidate after N seconds |
{ next: { tags: ['tag'] } } |
Tag-based revalidation |
Parallel Data Fetching
// Fetch in parallel, not sequentially
export default async function DashboardPage() {
const [users, posts, analytics] = await Promise.all([
getUsers(),
getPosts(),
getAnalytics()
]);
return (
<Dashboard users={users} posts={posts} analytics={analytics} />
);
}
Layouts and Templates
Root Layout (Required)
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My App',
description: 'App description'
};
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>...</header>
<main>{children}</main>
<footer>...</footer>
</body>
</html>
);
}
Nested Layouts
// app/dashboard/layout.tsx
export default function DashboardLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<Sidebar />
<div className="content">{children}</div>
</div>
);
}
API Routes
Route Handler
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function GET() {
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 Route Handler
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.user.findUnique({
where: { id: params.id }
});
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(user);
}
Error Handling
Error Boundary
// app/dashboard/error.tsx
'use client';
export default function Error({
error,
reset
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Not Found
// app/users/[id]/page.tsx
import { notFound } from 'next/navigation';
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await getUser(params.id);
if (!user) {
notFound();
}
return <UserProfile user={user} />;
}
Streaming and Suspense
Loading UI
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />;
}
Streaming with Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<ChartSkeleton />}>
<SlowChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<SlowTable />
</Suspense>
</div>
);
}
Additional Resources
Reference Files
For detailed patterns:
references/caching-strategies.md- Advanced caching patternsreferences/authentication.md- Auth patterns with Next.jsreferences/testing-patterns.md- Testing Next.js 16+ apps
Key Points
- Server Components are the default—add 'use client' only when needed
- Use Server Actions for mutations instead of API routes
- Leverage streaming and Suspense for better UX
- Use route groups to organize without affecting URLs
- Parallel fetch data when possible