Next.js
Server components changed how I structure a Next.js app
After six App Router projects, the folder structure I keep arriving at — and the three mistakes I made in the first two that cost me the most time.
Mr. Rofiq Rahman · · 11 min read
I have now built six production apps on the App Router. The first two I structured like a Pages Router app with new syntax, and I paid for it. Here is where I ended up.
The rule that fixed everything
Server components fetch and compose. Client components handle interaction. The boundary is as deep in the tree as it can be.
Almost every problem I had in those first two projects came from putting "use client" too high. One interactive dropdown in a header turned the entire page into client-rendered JavaScript, and I did not notice for weeks because it still worked.
The structure
app/
(site)/ server components, the public site
admin/
_actions/ server actions, grouped by domain
_components/ admin-only UI
api/ only where a route handler is genuinely needed
lib/
content/ read queries for the public site
db/ models and the connection
validation/ zod schemas shared by forms and actions
components/ui/ design-system primitives
The thing worth copying is lib/validation. One schema validates the form in the browser and the payload on the server. When they drift, you get a type error instead of a bug.
Three mistakes
1. A context provider at the root. It makes everything below it a client component. If you need theme state, put the provider around the smallest subtree that uses it — or push the state into a cookie and read it on the server.
2. Fetching in a client component because it was easier. It was easier for twenty minutes. Then it was a loading spinner, a race condition and a waterfall. Fetch on the server, pass data down.
3. Treating server actions as a REST API. An action is a mutation plus a revalidation. If you find yourself returning rich data from one to drive the UI, you probably wanted a server component.
Where I still hesitate
Deeply interactive views — a drag-and-drop builder, a live chat panel — are client components all the way down, and that is correct. Trying to be clever with the boundary there buys nothing and costs clarity.
The split is not about minimising client JavaScript for its own sake. It is that data-fetching on the server removes a whole category of state you no longer have to manage.