nextjs-app-router
SKILL.md
Next.js App Router Patterns
Project Structure
app/
├── (auth)/ # Route Group
│ ├── login/page.tsx
│ ├── register/page.tsx
│ └── layout.tsx
├── (dashboard)/
│ ├── layout.tsx
│ ├── page.tsx
│ └── [projectId]/
│ └── page.tsx
├── api/
│ └── webhooks/route.ts
├── layout.tsx
├── page.tsx
├── loading.tsx
├── error.tsx
└── not-found.tsx
Server vs Client Components
Decision Tree
- Need interactivity (onClick, useState)? -> 'use client'
- Need browser APIs? -> 'use client'
- Otherwise -> Server Component (default)
Server Component
// No directive needed - Server Component by default
import { prisma } from '@/lib/db'
export default async function UsersPage() {
const users = await prisma.user.findMany()
return <UserList users={users} />
}
Client Component
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
Server Actions
// lib/actions/users.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createUser(formData: FormData) {
const email = formData.get('email') as string
await prisma.user.create({ data: { email } })
revalidatePath('/users')
redirect('/users')
}
Using in Forms
import { createUser } from '@/lib/actions/users'
export function CreateUserForm() {
return (
<form action={createUser}>
<input name="email" type="email" required />
<button type="submit">Create</button>
</form>
)
}
Data Fetching
Parallel Fetching
export default async function Dashboard() {
const [user, posts] = await Promise.all([
getUser(),
getPosts()
])
return <DashboardView user={user} posts={posts} />
}
Streaming with Suspense
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
</div>
)
}
Caching
// Revalidate every 60 seconds
fetch(url, { next: { revalidate: 60 } })
// No caching
fetch(url, { cache: 'no-store' })
// Static (default)
fetch(url)
Protected Routes
// app/(dashboard)/layout.tsx
import { redirect } from 'next/navigation'
import { auth } from '@/lib/auth'
export default async function DashboardLayout({ children }) {
const session = await auth()
if (!session) redirect('/login')
return <div>{children}</div>
}
Weekly Installs
3
Repository
sabahattinkalka…stack-hqGitHub Stars
13
First Seen
Feb 14, 2026
Security Audits
Installed on
claude-code2
gemini-cli2
antigravity2
mcpjam1
openhands1
zencoder1