← All writing

How I Structure Large Next.js Applications

Every Next.js project I ship starts out small, and every one of them eventually grows past the point where "just put it in components" holds up. The projects I maintain now - a university student portal, a trading journal, a dev marketplace - all settled on roughly the same shape, and it's the shape I'd recommend to anyone past their first few routes.

Group by feature, not by file type

The instinct coming from smaller projects is to have one components/ folder, one hooks/ folder, one lib/ folder, and drop everything in. That works until you're scrolling past forty unrelated files trying to find the three that belong to checkout. Instead, I keep app/ organized by route the way Next.js wants it, and then group anything feature-specific - a form, its hooks, its API helpers - under a components/<feature>/ folder that mirrors the route it serves. Genuinely shared primitives (buttons, inputs, a status pill) are the only things that earn a spot in a flat components/ui/ folder.

One api.js, not fetch calls scattered everywhere

I keep a single lib/api.js that wraps every backend call behind a plain function - listProjects(), checkout(), adminUpdateOrderStatus() - so components never construct a fetch() call themselves. This does two things: it gives me one place to change the base URL, auth headers, or error handling, and it makes the API surface of the frontend readable in one file instead of grep-ing across the whole app for fetch(.

Server components for data, client components for interaction

I default every new file to a server component and only add "use client" when something actually needs state, effects, or a browser API. In practice that means page.jsx does the initial data fetch on the server (better first paint, works without JS, cheaper SEO) and hands the data down to a client component that owns the interactive parts - forms, filters, anything with an onClick. The project detail page in my marketplace app is a good example: the page itself is a server component that fetches the project for metadata, and a ProjectDetailClient handles the checkout form and re-fetches if that server call ever fails.

Keep the backend's shape out of the frontend's head

Pydantic schemas and SQLAlchemy models change more often than I'd like mid-project. I don't let components reach into raw API responses; everything passes through the api.js layer and, where the shapes diverge, gets normalized right there. It's saved me more than once when a backend field got renamed and I only had to touch one file.

None of this is exotic - it's the same advice you'll find in most Next.js style guides. What matters is applying it consistently from the second route onward, not waiting until the project is unpleasant to navigate.