Next.js App Router
Project context
This is a Next.js 15 App Router project. The codebase uses Server Components, Server Actions, and the file-based App Router conventions exclusively. AI assistants frequently confuse App Router with Pages Router — these rules exist to prevent that.
Stack
- Next.js 15 (App Router)
- React 19 (Server Components first)
- TypeScript strict mode
- Tailwind 4 for styling
- pnpm as the package manager
Folder layout
src/app/
layout.tsx — root layout (every route inherits)
page.tsx — root page
(marketing)/ — route group, doesn't affect URL
blog/[slug]/page.tsx — dynamic route
api/route.ts — only for public APIs and webhooks
loading.tsx — Suspense fallback for the segment
error.tsx — error boundary for the segment
not-found.tsx — 404 for the segment
Co-locate route-specific helpers next to the page (actions.ts, schema.ts, components folder). Use route groups (name)/ to organize routes without affecting URLs.
Server Components vs Client Components
Default to Server Components. They render on the server, ship zero JS, and can await directly.
export default async function Page() {
const data = await getData()
return <Content data={data} />
}
Add "use client" only when needed:
- Component uses
useState,useEffect,useRef - Component handles browser events (
onClick,onChange) - Component uses browser-only APIs (
window,localStorage,navigator) - Component uses third-party libraries that depend on the above
Push the client boundary as deep as possible. A page can be a server component that renders a small interactive <Toggle /> client component. Don't mark the whole page client just to have one button.
Never import 'server-only' from a file that gets pulled into a client component. The error message is opaque; check the import graph if you see "ReactServerComponentsError".
Data fetching
- Fetch in server components with
await - Use
fetch()directly — Next.js automatically dedupes and caches per-request - Configure caching explicitly:
fetch(url, { next: { revalidate: 60 } })or{ cache: 'no-store' } - For database queries, call your DB client directly from a server component
- Pass server-fetched data as props to client components — never re-fetch in a
useEffect
Server Actions
Use Server Actions for form submissions and mutations.
// app/posts/new/actions.ts
'use server'
import { z } from 'zod'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
const Schema = z.object({ title: z.string().min(1).max(200) })
export async function createPost(formData: FormData) {
const parsed = Schema.parse({ title: formData.get('title') })
await db.posts.create({ data: parsed })
revalidatePath('/posts')
redirect('/posts')
}
- Validate input with Zod at the action boundary
- Call
revalidatePath()orrevalidateTag()after mutations - Use
redirect()to navigate after success
Route Handlers
Route handlers (route.ts) are for public APIs only:
- Webhooks (Stripe, Clerk, etc.)
- Public REST endpoints consumed by external clients
- Well-known files (
robots.txt,sitemap.xml)
Don't use route handlers as a private internal API for your own client components — use Server Actions instead.
Metadata & SEO
- Export
metadatafrompage.tsxandlayout.tsx - For dynamic routes, export
generateMetadata(async) - Use
alternates: { canonical: ... }to declare canonical URLs - For OG images, use
opengraph-image.tsx(or.png/.jpg) co-located with the route - Never set
<title>or<meta>tags manually in JSX — use the metadata API
Caching
Three caches in App Router:
- Request memoization — within one request, identical
fetch()calls are deduped - Data Cache — persistent across requests; controlled via
fetchoptions - Full Route Cache — static rendering output
Be deliberate. If a page must be dynamic, opt in via export const dynamic = 'force-dynamic' or use cookies() / headers().
Patterns to avoid
getServerSideProps,getStaticProps,getInitialProps— Pages Router only_app.tsx,_document.tsx— replaced bylayout.tsxnext/router— usenext/navigationinstead (useRouter,usePathname,useSearchParams)<Head>fromnext/head— use the metadata API- Fetching in
useEffect— use server components or actions 'use client'at the layout root — push it down to the leaf
Testing
- Unit tests with Vitest, alongside source files
- E2E with Playwright for critical paths only
- Mock at the boundary (mock
fetch, not internal helpers) - Run
pnpm test && pnpm typecheck && pnpm lintbefore claiming done
Tooling
pnpm dev— dev server (uses Turbopack on 15+)pnpm build— production buildpnpm lint— ESLint witheslint-config-nextpnpm typecheck—tsc --noEmit
AI behavioral rules
- Confirm App Router before suggesting any Next.js feature; assume App Router unless told otherwise
- Never suggest
getServerSidePropsorgetStaticProps— they don't exist here - When fetching data, default to a server component
- When adding a mutation, default to a Server Action
- Push
"use client"to the smallest leaf component - Run lint, typecheck, and tests before declaring a task complete
- Don't add a feature flag or
// removedcomment when deleting code — just delete it