Feb 07, 2026·7 min read

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.

Next.js folder structure that still works after month six

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

Get Fractional CTO Help
Use experienced guidance to split routes, features, and shared code with clear rules.

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.

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

Stop Growing Misc Folders
Get a practical folder map for your product before the tree gets harder to trust.

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

Scale Without Repo Drift
Keep shipping while you tighten structure, ownership, and review rules.

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.

Frequently Asked Questions

When should I move code out of the app folder?

Move code out of app when it would still make sense after a URL change. Forms, queries, validation, and business rules usually belong in features/..., while app should hold pages, layouts, loading files, and route handlers.

What should stay at the project root?

Keep the root small and boring. Most teams do well with app, features, shared, plus project-wide config, scripts, and global type files. If a folder does not solve a clear project-level need, do not put it at the top.

How do I know a file belongs in a feature folder?

Ask who owns the rule or UI. If one product area owns it, keep the file inside that feature even when two routes use it. A billing validator still belongs to billing.

What actually counts as shared code?

Shared code should work in several parts of the product without product words baked in. Things like buttons, modals, useDebounce, or a date formatter fit well there. A BillingPlanCard or useCheckoutFlow does not.

Should I keep top-level components, hooks, and utils folders?

Usually no. Top-level buckets like components, hooks, and utils spread one feature across too many places. Put files near the feature instead, and keep shared for code that stays generic.

Where should server actions live?

Put server actions with the feature that owns the behavior. If billing updates a plan, keep that action in features/billing, not beside app/account/billing/page.tsx. Route files should wire pages, not hold business logic.

How do I clean up a messy repo without breaking everything?

Start with one busy route cluster and move related files in small batches. Fix imports and run the app after each move so you catch problems early. One feature at a time creates smaller diffs and easier reviews.

What are the signs our folder structure is drifting?

Watch the imports first. Long relative paths, vague folders like misc or utils2, and copied versions of the same pattern usually mean the tree has drifted. Another clue is when teammates cannot guess where a new file should go.

When should I move something from a feature into shared?

Wait until a second feature really uses the same code, then check whether the code still reads as generic. If it keeps product names, product imports, or product rules, leave it in the feature. Moving code too early fills shared with files that never become truly reusable.

Do we need a full rewrite to fix the structure?

Usually not. Most teams fix this with a calm cleanup, a short folder map, and review rules that stop new clutter. If the team keeps arguing about structure or breaking other areas during moves, a quick review from an experienced CTO can save time.