# Next.js folder structure that still works after month six

> Next.js folder structure gets messy fast as apps grow. Learn a simple way to separate routes, features, and shared code before chaos sets in.

## Why the tree gets messy after month six

A project rarely gets messy in one dramatic moment. It happens through a pile of small rush decisions. The team adds a few screens, a couple of API handlers, a new form, and two experiments in the same sprint. Nobody plans to create a confusing tree. People just keep shipping.

In the first month, the structure still feels easy. One person remembers where everything lives. By month six, the product has loading states, error files, server actions, form schemas, tests, analytics hooks, and one-off helpers that showed up during deadline pressure.

Most teams put new code near the route they touched last. That feels fast, and sometimes it is. If someone edits `app/dashboard/page.tsx`, they often drop the modal, table config, validation, and helper function right next to it because that folder is already open.

That shortcut adds up. A route folder starts as a page and turns into a storage closet. Now it holds UI pieces, business rules, fetch logic, type definitions, and code another screen also needs.

Then the copying starts. A team builds a filter bar for one area, then makes a slightly different version for another, then one more for admin. The same pattern now lives in three places with three names.

Imports usually tell you when the structure is drifting. Paths get longer and stranger. Files move into `shared`, `common`, or `utils` because nobody knows where else to put them. Those folders are often misc directories with better labels.

Ownership gets blurry too. If a file sits under a route, does that route own it? If three screens use it, should it move? Teams usually ask those questions late, after someone changes a component and breaks another part of the app.

The App Router can make this worse because route folders mix framework files with product code. That is convenient early on, but route structure and feature structure are not the same thing.

A sane Next.js folder structure usually breaks down for a simple reason: the team grows faster than the rules for where code should live. Each new file looks harmless on its own, but the tree gets harder to trust every week.

## What belongs at the top level

The root of the project should stay boring. If every new file lands there, the structure falls apart fast and nobody knows where to look after a few months.

Keep the top level limited to folders that answer a few simple questions: Where do routes start? Where does feature code live? What code do several areas actually share? What runs the project outside the app itself?

A setup like this usually holds up well:

- `app` for routes, layouts, templates, loading states, and page entry points
- `features` for product areas such as auth, billing, onboarding, or dashboard
- `shared` for code that several areas already use
- `scripts`, config files, and global type files near the root

That split keeps each folder honest. The `app` folder should describe URL structure and page wiring, not business logic. If a billing page needs rules, forms, API calls, and tests, those pieces should sit in `features/billing`, not beside the route file.

The `features` folder works best when each feature feels like a small product slice. A new developer should be able to open `features/auth` and find the auth UI, actions, hooks, and tests in one place. That saves time and cuts down on the classic hunt across `components`, `utils`, `hooks`, and `services`.

`shared` needs the strictest rule. Put something there only after two or three parts of the product already use it. A button, date formatter, or API client wrapper can belong there. A helper used only by billing should stay in billing.

Keep project-wide files outside features because they do not belong to one product area. That includes linting config, TypeScript config, environment setup, build scripts, and files like `global.d.ts`. On larger teams, this matters even more. People move faster when the root stays small and each folder has one job.

## How to split routes from features

If a route folder starts collecting form logic, data fetching, validation, and helper files, it stops being a map of URLs. It becomes a second app hidden inside `app`, and that is usually where the mess starts.

Keep `app` focused on navigation. A route should tell Next.js what URL exists, what layout wraps it, and which feature entry point to render. Product rules should live somewhere else.

A good test is simple: if the code would still make sense after a URL change, it does not belong in `app`. A signup form, pricing calculator, invoice query, or permission check is part of a feature. It is not part of `/signup` or `/billing`.

A clean split is straightforward. `app` holds pages, layouts, loading states, and route handlers. `features` holds product areas like auth, billing, onboarding, or reports. Each feature keeps its components, schema, server actions, queries, and tests together.

This keeps changes local. If you rename `/settings/billing` to `/account/billing`, you move a thin route file instead of dragging a whole pile of logic with it.

Each feature folder should feel complete. `features/billing` might contain `BillingForm.tsx`, `billing.schema.ts`, `billing.actions.ts`, `billing.queries.ts`, and `billing.test.ts`. Then `app/account/billing/page.tsx` can import a `BillingPage` component from that folder and stay boring. Boring route files are a good sign.

Teams often avoid this split because it looks like extra structure. Six months later, it saves time. New people can scan the tree quickly, and fewer files end up in random route folders because there is already an obvious home for them.

Do not turn `features` into a second junk drawer. If code is specific to billing, keep it in billing even if two or three routes use it. Move code to `shared` only when it is truly generic and still makes sense without any product context around it.

Routes describe URLs. Features hold the work. That rule is simple, and it lasts.

## What shared code should and should not hold

Shared code should stay boring too. If a file carries product rules, page-specific behavior, or business names, it does not belong in `shared`.

Without that rule, `shared` turns into a junk drawer where old code goes to hide.

### What belongs in shared

A file earns a place in `shared` only if several checks are already true. More than one feature uses it today. It has no product-specific language. It does not import from a feature folder. You could move it to another project with little or no change.

For UI, keep `shared/ui` small and plain. Buttons, modals, form fields, avatars, tabs, and layout primitives fit there. A `BillingPlanCard` does not. Even if two billing pages use it, that component still belongs to the billing feature because its meaning comes from the product.

The same rule works for hooks. Put generic hooks in `shared/hooks`, such as `useDebounce`, `useMediaQuery`, or `useLocalStorage`. Move hooks like `useCheckoutFlow`, `useWorkspaceMembers`, or `useProjectLimits` back into their feature folders. Those hooks know too much about one part of the app.

A simple test helps: rename the file without business words. If the name becomes vague or silly, it probably is not shared code.

### What shared should reject

Folders like `helpers`, `utils`, and `common` usually cause the mess. They sound harmless, but they do not tell anyone what belongs there. Soon you get `date.ts`, `format.ts`, `api.ts`, `misc.ts`, and a few half-used hooks nobody wants to touch.

Pick folders by purpose instead. Use names like `shared/ui`, `shared/hooks`, `shared/lib`, and `shared/config`. Then keep each folder strict. `shared/lib` can hold small pure functions, but not billing math tied to your pricing model.

This line matters even more on a startup team. You can move fast without chaos if product logic stays inside features and shared code stays generic. When somebody adds `useInvoicesTableState` to `shared/hooks`, send it back to `features/invoices` before three more files gather around it.

## A step-by-step cleanup plan

Start with the routes people use most. They expose the mess fastest because they pull code from the most places. Open two or three busy screens, trace every component, hook, helper, schema, and server action they import, and write that list down. A short audit usually shows where the structure started to drift.

Then sort those files by product area, not by file type. If a billing page uses a form, a validator, a server action, and a couple of UI pieces, that work belongs near billing. Splitting everything into `components`, `hooks`, `utils`, and `lib` feels tidy at first, but it makes one change spread across half the repo.

Use a narrow cleanup loop:

1. Pick one route cluster, such as `app/dashboard/billing`.
2. Create one feature folder for that area.
3. Move related files in small batches.
4. Fix imports after each batch and run the app right away.

Keep the scope tight. If you move five areas at once, you get a huge diff, broken paths, and a review nobody wants to touch. One feature at a time feels slower for a day and faster for the next six months.

When you hesitate, use a simple test. If only one product area uses a file, keep it inside that feature. If several areas use it, check whether they truly share the same rule or only look similar. Teams often push almost-finished code into `shared` too early, then spend months working around it.

After the first move, write a few folder rules before new work lands. Keep them short enough that people will actually read them: routes describe URL structure, features hold code for one product area, shared holds stable code used by several areas, and new files start near the feature that needs them.

This does not need a big architecture meeting. A small team can clean one feature per week and stop the misc folder from growing again. Growing products usually recover through a calm series of moves, not a heroic rewrite.

## A simple example from a growing product

A dashboard often starts small. Month one has a users page, a couple of forms, and a route file that does too much. By month six, the same product adds billing and settings, and the old `components`, `lib`, and `utils` folders start filling with unrelated code.

Say the app has `/dashboard/users`, then adds `/dashboard/billing` and `/dashboard/settings`. The mistake is to keep pushing billing code into shared places just because it feels reusable. Billing usually needs its own table, payment form, input rules, and server actions, so it should live together as one feature.

```text
app/
  dashboard/
    users/page.tsx
    billing/page.tsx
    settings/page.tsx

features/
  billing/
    components/
      billing-table.tsx
      payment-form.tsx
    lib/
      validators.ts
      actions.ts
  users/
    components/
      users-table.tsx

shared/
  ui/
    button.tsx
    modal.tsx
  lib/
    currency.ts
```

With this setup, `app/dashboard/billing/page.tsx` stays short. It reads the route params, checks access if needed, and renders the billing feature. The page file does not own the table markup, the form rules, or the server action that updates a plan.

The shared folder stays narrow. Put a button there because every part of the app can use it. Put a modal there for the same reason. A currency formatter also fits if users, billing, and reports all need the same output. Do not move `billing-table.tsx` or `payment-form.tsx` into shared just because another billing screen might reuse them. They still belong to billing.

This is where the structure starts to feel easier to live with. New work has an obvious home. When billing grows, the team adds files inside `features/billing` instead of dumping them into `components` or `lib` and hoping the names stay clear.

## Mistakes that rebuild the misc folder

A messy tree rarely appears in one big mistake. It grows from small shortcuts that feel harmless for a week and annoying for the next six months.

One common problem is random server actions. A team adds an action inside a route folder because that page needs it today, then adds another action for a different flow in another route tomorrow. Soon the app directory holds business logic, form handling, validation, and route files all mixed together. If an action belongs to billing, auth, or onboarding, keep it with that feature and let the route file stay thin.

Names cause trouble too. Folders called `misc`, `temp`, `helpers`, or `utils2` are junk drawers with better branding. They do not tell anyone what belongs there, so people keep tossing files in. A good name makes the decision for you. If you cannot name a folder by purpose, the code probably belongs somewhere more specific.

Another easy way to rebuild the mess is to move only half a feature. Someone starts a cleanup and pulls UI components into `features/billing`, but leaves hooks in the old route folder and types in `shared` because moving them feels optional. A month later, billing lives in three places. The next person copies a file instead of chasing references across the project.

I would rather see one slightly imperfect move than a half-finished clean one. When you relocate a feature, move the components, actions, tests, types, and helpers together unless you have a clear reason not to.

Sharing code too early creates a quieter version of the same problem. A helper gets pushed into `shared` after one reuse because it might be useful later. Most of the time, it never gets reused again. Then `shared` turns into a storage unit for code that belongs to one feature but lost its home.

A simple rule works well: keep code inside the feature until a second feature really needs it, rename vague folders before they collect more files, finish moves in one pass, and treat route folders as entry points, not storage.

## Quick checks before you add a file

Pause for 20 seconds before you create a new file. That tiny pause saves hours later, because most messy trees start with one harmless shortcut the whole team repeats.

Start with reuse. If only one route needs the file now and will likely own it later, keep it near that route. A checkout form used only in billing should live with billing. If you move it into shared too early, people waste time searching for it, then copy it into the feature anyway.

Then check what the file actually does. Generic behavior belongs in shared code. Product rules do not. A date formatter, debounce helper, or plain input wrapper fits in shared space. A function that decides whether a user can upgrade, see a trial, or unlock a report belongs with the feature because that logic explains the product itself.

Folder names need the same pressure test. If a name feels vague now, it will age badly. `utils`, `misc`, `common`, and `helpers` look harmless for a week. After month six, they turn into junk drawers.

The structure should make placement boring. A new teammate should guess the file location in under a minute. If they need to ask in Slack, click through six folders, or open random files, the structure already failed.

A quick gut check is enough: keep route-only code with the route, keep product logic with the feature, put only truly generic code in shared folders, and rename vague folders before they collect more files.

Small example: a `PlanCard` used on pricing, onboarding, and account pages may belong in shared UI. A `canUpgradePlan()` rule does not. Even if three pages call it, the rule still belongs to the billing or subscriptions feature, not a generic helpers folder.

Teams often overthink this. You do not need the perfect place forever. You need a place that still makes sense three months from now. If two teammates would put the same file in two different folders, fix the folder names before you commit.

## What to do next with your team

A clean structure does not survive on good intentions. It survives when the team uses the same simple rules every week, especially when deadlines get tight.

Start with a one-page folder map. Keep it short enough that a new developer can read it in two minutes. Show the main folders, what each one is for, and a few real examples: where a route file lives, where a feature component goes, and where shared helpers stop.

A small set of rules is usually enough. Put route files in `app` and keep them thin. Put feature code near the feature, not in a generic `utils` or `components` bucket. Move something to shared only after at least two parts of the product use it. If a file name needs a long explanation, it probably sits in the wrong place.

Then use those rules in pull request reviews. Do not turn review into a style debate. Ask one direct question: "Will this file still make sense to someone else in three months?" If the answer is no, fix the location before merge. That saves a lot of slow cleanup later.

Pick a cleanup day before the next big feature starts. One afternoon is often enough to rename folders, move obvious strays, and delete dead code. Doing this before a larger build matters. Once a feature lands on top of a messy tree, the mess gets baked in.

A shared habit helps too. When someone is unsure where a file belongs, they should ask in the pull request instead of guessing. Ten minutes of discussion beats six months of everyone copying the same bad pattern.

If the app already feels tangled, an outside review can help. Oleg Sotnikov at oleg.is works as a fractional CTO and startup advisor for growing teams, and this is exactly the kind of problem where a practical second opinion helps. Sometimes the fix is not a big rewrite. It is a clear folder map, a few hard rules, and a team that follows them.
