nextjs-patterns
SKILL.md
Next.js Patterns
Modern Next.js 15+ App Router patterns and best practices.
Server vs Client Components
Default to Server Components
// app/users/page.tsx (Server Component by default)
export default async function UsersPage() {
const users = await db.user.findMany(); // Direct DB access
return <UserList users={users} />;
}
Client Components for Interactivity
'use client';
// Only add 'use client' when you need:
// - useState, useEffect, useRef
// - Event handlers (onClick, onChange)
// - Browser APIs (window, localStorage)
// - Third-party client libraries
export function SearchBar() {
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Composition Pattern
// Server Component (parent) passes data to Client Component (child)
export default async function Page() {
const data = await fetchData(); // Server-side
return <InteractiveChart data={data} />; // Client-side interactivity
}
Data Fetching
Server Component Fetching
// Direct async/await in Server Components
async function UserProfile({ userId }: { userId: string }) {
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) notFound();
return <div>{user.name}</div>;
}
Parallel Data Fetching
export default async function Dashboard() {
// Fetch in parallel, not waterfall
const [users, stats, recent] = await Promise.all([
getUsers(),
getStats(),
getRecentActivity(),
]);
return (
<>
<UserList users={users} />
<StatsPanel stats={stats} />
<ActivityFeed items={recent} />
</>
);
}
Server Actions
// 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;
// Validate
const validated = UserSchema.parse({ name, email });
// Write to database
await db.user.create({ data: validated });
// Revalidate
revalidatePath('/users');
}
Caching and Revalidation
Route Segment Config
// Static page (default)
export const dynamic = 'auto';
// Always dynamic (no cache)
export const dynamic = 'force-dynamic';
// Revalidate every 60 seconds
export const revalidate = 60;
On-Demand Revalidation
import { revalidatePath, revalidateTag } from 'next/cache';
// Revalidate specific path
revalidatePath('/users');
// Revalidate by tag
revalidateTag('users');
Middleware
// middleware.ts (root level)
import { NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// Authentication check
const token = request.cookies.get('session');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add security headers
const response = NextResponse.next();
response.headers.set('X-Frame-Options', 'DENY');
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
File Structure
app/
├── layout.tsx # Root layout (shared UI)
├── page.tsx # Home page
├── loading.tsx # Loading UI (Suspense)
├── error.tsx # Error boundary
├── not-found.tsx # 404 page
├── (auth)/ # Route group (no URL segment)
│ ├── login/page.tsx
│ └── register/page.tsx
├── dashboard/
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx
│ └── settings/page.tsx
└── api/
└── users/route.ts # API route handler
Checklist
- Server Components by default (add 'use client' only when needed)
- Parallel data fetching with Promise.all
- Server Actions for mutations
- Proper loading.tsx and error.tsx at each route level
- Middleware for auth and security headers
- Caching configured per route
- Metadata exported for SEO
- Images use next/image with proper sizing
Weekly Installs
2
Repository
peopleforrester…dotfilesGitHub Stars
1
First Seen
Feb 28, 2026
Security Audits
Installed on
opencode2
gemini-cli2
codebuddy2
github-copilot2
codex2
kimi-cli2