Why the App Router feels confusing at first
If you learned React with pages/ or Create React App, the App Router can feel like three frameworks stacked together: React Server Components, file-based routing, and a new data-fetching story.
The cure is not more blog posts with the word “modern.” The cure is a small mental model you can reuse every time you touch a file.
The core idea
In the App Router:
- Files under
app/are routes and UI for those routes. - Components are Server Components by default.
- You opt into the browser with
"use client"only when you need browser APIs, React state/effects, or event handlers that cannot stay on the server. - Mutations and secured work usually live in Route Handlers (
app/api/.../route.ts) or Server Actions — not scatteredfetchcalls inside random client components.
If you remember only one sentence: keep as much as possible on the server; push the minimum interactive island to the client.
page.tsx vs layout.tsx vs Client Component
layout.tsx
Shared chrome for a segment: header shell, dashboard sidebar wrapper, fonts providers at the root. Layouts persist across navigations inside that segment, which is why dashboards feel snappy when done right.
page.tsx
The unique UI for that URL. Prefer fetching data here (or in a small server-only module the page calls). Pass serializable props into client children.
Client Component ("use client")
Use for:
- Forms with controlled inputs and live validation UI
- Modals, tabs, carousels driven by state
- Charts, canvas, Framer Motion with hover/gesture state
- Anything that needs
useSession,useRouterevents, orwindow
Do not mark your entire app client “just in case.” That throws away streaming, smaller bundles, and clean server data access.
Data fetching without the folklore
A practical default at Desi Stack:
- Server page loads session + DB data with Prisma.
- Serialize Decimal/Date fields when needed.
- Hand plain objects to a
*Client.tsxfor interactivity. - Mutations hit validated API routes protected by auth checks.
Avoid “fetch in useEffect on every page mount” for core content. That pattern reintroduces loading waterfalls and SEO pain for public pages.
For admin tables where filters are chatty, client-side filtering of an initial server payload (or TanStack Query against admin APIs) is fine — clarity over purity.
Route Handlers in plain English
app/api/admin/blogs/route.ts is not “backend from 2015.” It is how you:
- Centralize Zod validation
- Call
checkAdminAccess() - Keep secrets off the client
- Return JSON the UI can trust
Treat each route like a small use-case endpoint: create post, verify payment, sync game session.
Boundaries cheat sheet
| Need | Prefer |
|---|---|
| Read DB for a public page | Server Component / page.tsx |
| Button that opens a modal | Client Component |
| Admin-only write | API route + admin auth |
| Shared shell | layout.tsx |
| Loading UX while navigating | loading.tsx skeleton |
| SEO title per article | generateMetadata |
Common mistakes we see
"use client"at the root layout — usually wrong; isolate islands instead.- Passing functions or class instances as props from server to client — props must be serializable.
- Fetching secrets in client components — never.
- No
loading.tsx— users think the app froze during slow DB calls. - Giant client forms with no server validation — always validate again on the API.
A 10-minute practice drill
- Open any feature page in this repo.
- Label each file: Server / Client / API / Layout.
- Ask: “Could this
useEffectfetch move to the server page?” - If a client file is over ~300 lines of mixed UI + fetch + mutation, split it.
When the labels feel obvious, the App Router stops being scary — it becomes a sorting exercise.
Next steps
Build something real with this model: a project template, a blog post pipeline, or a dashboard slice. Theory only sticks when a deploy depends on it.