Next.js App Router Patterns
File Conventions
| File | Purpose |
|---|---|
page.tsx |
Route UI, receives params/searchParams |
layout.tsx |
Shared UI, wraps children, persists across nav |
loading.tsx |
Suspense fallback for the segment |
error.tsx |
Error boundary — must be 'use client' |
not-found.tsx |
404 UI — call notFound() to trigger |
route.ts |
API endpoint (GET, POST, etc.) |
Server Actions
// ✅ CORRECT
"use server"
import { z } from "zod"
const schema = z.object({ name: z.string().min(1) })
export async function updateName(formData: FormData) {
const result = schema.safeParse({ name: formData.get("name") })
if (!result.success) {
return { success: false, error: result.error.flatten().fieldErrors }
}
await db.update(result.data)
revalidatePath("/profile")
return { success: true }
}
// ❌ WRONG
export async function updateName(formData: FormData) {
await db.update({ name: formData.get("name") as string })
}
Rules: Always "use server" · Always Zod validate · Always {success, error} shape · Never return sensitive data
Route Handlers
// ✅ CORRECT
export async function POST(req: NextRequest) {
try {
const { email } = bodySchema.parse(await req.json())
await subscribe(email)
return NextResponse.json({ success: true })
} catch (err) {
if (err instanceof z.ZodError) {
return NextResponse.json({ error: err.flatten() }, { status: 400 })
}
return NextResponse.json({ error: "Internal error" }, { status: 500 })
}
}
Image Optimization
// ✅ CORRECT — always next/image
import Image from "next/image"
<Image src="/hero.jpg" alt="Hero" width={1200} height={630} priority />
// ❌ WRONG
<img src="/hero.jpg" alt="Hero" />
priority on above-the-fold images (improves LCP). Always width + height to prevent CLS.
Font Optimization
// ✅ CORRECT — no FOUT, self-hosted, no privacy leak
import { Inter } from "next/font/google"
const inter = Inter({ subsets: ["latin"], display: "swap" })
<html lang="he" dir="rtl" className={inter.className}>
use client Boundary Rules
// ✅ Push to leaf components only
// page.tsx — Server Component
export default async function Page() {
const data = await db.query()
return <LikeButton />
}
// LikeButton.tsx
"use client"
export function LikeButton() {
const [liked, setLiked] = useState(false)
return <button => setLiked(true)}>Like</button>
}
// ❌ WRONG — entire page becomes client
"use client"
export default function Page() { ... }
Common Mistakes
| Mistake | Fix |
|---|---|
useState in Server Component |
Add "use client" or move to child |
router.push() in Server Action |
Use redirect() from next/navigation |
searchParams in layout |
Only available in page.tsx |
new Response() from route |
Use NextResponse.json() |
<img> for content images |
Use next/image always |