)
}
```
### Not Found
```tsx
// app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation'
export default async function PostPage({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
if (!post) notFound()
return {post.content}
}
```
---
## Server Actions
### Defining Actions
```tsx
// app/actions.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
})
export async function createPost(formData: FormData) {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
const parsed = schema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
})
if (!parsed.success) return { error: parsed.error.flatten() }
const post = await db.post.create({
data: { ...parsed.data, authorId: session.user.id },
})
revalidatePath('/posts')
redirect(`/posts/${post.slug}`)
}
```
### Form with useFormState and useFormStatus
```tsx
// components/submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
)
}
// components/create-post-form.tsx
'use client'
import { useFormState } from 'react-dom'
import { createPost } from '@/app/actions'
export function CreatePostForm() {
const [state, formAction] = useFormState(createPost, {})
return (
)
}
```
### Optimistic Updates
```tsx
'use client'
import { useOptimistic, useTransition } from 'react'
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [isPending, startTransition] = useTransition()
const [optimisticTodos, addOptimistic] = useOptimistic(
initialTodos,
(state, newTodo: string) => [...state, { id: 'temp', title: newTodo, completed: false }]
)
async function handleSubmit(formData: FormData) {
const title = formData.get('title') as string
startTransition(async () => {
addOptimistic(title)
await addTodo(formData)
})
}
return (
<>
{optimisticTodos.map(todo => (
{todo.title}
))}
>
)
}
```
### Revalidation
```tsx
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function updatePost(id: string, formData: FormData) {
await db.post.update({ where: { id }, data: { ... } })
revalidateTag(`post-${id}`) // Invalidate by cache tag
revalidatePath('/posts') // Invalidate specific page
revalidatePath(`/posts/${id}`) // Invalidate dynamic route
revalidatePath('/posts', 'layout') // Invalidate layout and all pages under it
}
```
---
## Route Handlers (API Routes)
### Basic CRUD
```tsx
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const page = parseInt(searchParams.get('page') ?? '1')
const limit = parseInt(searchParams.get('limit') ?? '10')
const [posts, total] = await Promise.all([
db.post.findMany({ skip: (page - 1) * limit, take: limit }),
db.post.count(),
])
return NextResponse.json({ data: posts, pagination: { page, limit, total } })
}
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 })
}
```
### Dynamic Route Handler
```tsx
// app/api/posts/[id]/route.ts
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await db.post.findUnique({ where: { id } })
if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(post)
}
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
await db.post.delete({ where: { id } })
return new NextResponse(null, { status: 204 })
}
```
### Streaming / SSE
```tsx
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ count: i })}\n\n`))
await new Promise(r => setTimeout(r, 1000))
}
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
})
}
```
---
## Parallel and Intercepting Routes
### Parallel Routes (Slots)
```
app/
├── @modal/
│ ├── (.)photo/[id]/page.tsx # Intercepted route (modal)
│ └── default.tsx
├── photo/[id]/page.tsx # Full page route
├── layout.tsx
└── page.tsx
```
```tsx
// app/layout.tsx
export default function Layout({ children, modal }: {
children: React.ReactNode
modal: React.ReactNode
}) {
return <>{children}{modal}>
}
```
### Modal Component
```tsx
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
router.back()}>
e.stopPropagation()}>
{children}
)
}
```
---
## Authentication (NextAuth.js v5 / Auth.js)
### Setup
```tsx
// auth.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Credentials from 'next-auth/providers/credentials'
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
Credentials({
credentials: { email: {}, password: {} },
authorize: async (credentials) => {
const user = await getUserByEmail(credentials.email as string)
if (!user || !await verifyPassword(credentials.password as string, user.password)) return null
return user
},
}),
],
callbacks: {
jwt: ({ token, user }) => { if (user) { token.id = user.id; token.role = user.role } return token },
session: ({ session, token }) => { session.user.id = token.id as string; session.user.role = token.role as string; return session },
},
})
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers
```
### Middleware Protection
```tsx
// middleware.ts
export { auth as middleware } from '@/auth'
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}
```
### Server Component Auth Check
```tsx
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const session = await auth()
if (!session) redirect('/login')
return
Welcome, {session.user?.name}
}
```
### Server Action Auth Check
```tsx
'use server'
import { auth } from '@/auth'
export async function deletePost(id: string) {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
const post = await db.post.findUnique({ where: { id } })
if (post?.authorId !== session.user.id) throw new Error('Forbidden')
await db.post.delete({ where: { id } })
revalidatePath('/posts')
}
```
---
## Route Segment Config
```tsx
export const dynamic = 'force-dynamic' // 'auto' | 'force-dynamic' | 'error' | 'force-static'
export const revalidate = 3600 // seconds
export const runtime = 'nodejs' // or 'edge'
export const maxDuration = 30 // seconds
```
---
## Anti-Patterns to Avoid
1. ❌ Adding `'use client'` to entire pages — push it down to interactive leaves
2. ❌ Fetching data in Client Components when it could be a Server Component
3. ❌ Sequential `await` when fetches are independent — use `Promise.all()`
4. ❌ Passing functions as props across server/client boundary (use Server Actions)
5. ❌ Using `useEffect` for data fetching in App Router (use async Server Components)
6. ❌ Forgetting `await params` in Next.js 15 (they're Promises now)
7. ❌ Missing `loading.tsx` or `` boundaries for async pages
8. ❌ Not validating Server Action inputs (always validate with zod)