Skip to main content
ASoc
Tutorial

Next.js 13 Folder Structure: 44 Files in app/, None of Them Components

27 pages, 4 route handlers, 13 metadata files in app/ — and not one component. The real tree behind 111 products and 337 posts, plus the rule that keeps it honest.

The ASoc Team11 min read

A Next.js 13+ folder structure has exactly one rule the framework enforces: inside app/, nested folders define routes, and a folder becomes a URL only when it contains a page.tsx or route.ts. Everything else — src/ or no src/, where components live, how deep to nest — is yours to decide, and the framework will not stop you getting it wrong.

The short answer

Put only route files in app/ and every component in a sibling tree with a declared import direction. This storefront's app/ holds 44 files: 27 pages, 4 route handlers, 2 layouts, 8 metadata and error files, and 3 assets. Not one of them is a component. The 92 components live in src/components, layered, with the direction of imports checked by tests.

That split is the whole decision. The Next.js docs permit colocating components inside app/ — "a route is not publicly accessible until a page.js or route.js file is added" — and it is true, it works. The reason not to is that colocation makes a folder's contents a judgement call, and a judgement call made 34 times across a growing tree stops agreeing with itself.

The tree, as it actually ships

This is not an illustrative example. It is this repository, which builds /, /templates, 111 product pages, 337 blog posts, 7 pSEO category hubs and a gated dashboard:

asoc-landing-page/
├── src/
│   ├── app/                 44 files — routes and metadata ONLY
│   ├── components/          92 files in 4 layers
│   │   ├── atoms/            7
│   │   ├── molecules/       41
│   │   ├── organisms/       33
│   │   └── templates/       11
│   ├── content/            337 files — MDX article bodies
│   ├── data/                20 files — typed content arrays + the catalog
│   ├── lib/                 68 files — logic, clients, 31 test files
│   ├── mdx-components.tsx       article typography (must be at src root)
│   └── proxy.ts                 Next 16 proxy, formerly middleware.ts
├── public/                      images, brand assets
├── scripts/                     audit · images · release · seo
├── supabase/migrations/      8 SQL files, 271 lines
└── next.config.ts

Four numbers in that tree are the ones worth copying:

DirectoryFilesWhat it is allowed to contain
src/app44page.tsx, route.ts, layout.tsx, the error/metadata specials, three assets
src/components92Components, nothing else — no data, no fetching helpers
src/data20Typed arrays. The only place section copy lives
src/lib68Everything that is not a component and not content

What belongs in app/, file by file

Next.js reserves specific filenames. These are the ones in use here, with the real paths, because "special files" lists in tutorials rarely say which you will actually need:

FileReal path hereJob
layout.tsxsrc/app/layout.tsxThe root layout — it must declare <html> and <body>; Next does not add them
page.tsxsrc/app/page.tsxMakes a segment a public route. 27 of these
route.tssrc/app/api/download/route.tsA handler instead of a page. 4 of these
not-found.tsxsrc/app/not-found.tsxBranded 404
error.tsxsrc/app/error.tsxClient error boundary for the segment
global-error.tsxsrc/app/global-error.tsxThe root boundary — replaces the layout, so it needs its own <html>
sitemap.tssrc/app/sitemap.tsGenerates /sitemap.xml
robots.tssrc/app/robots.tsGenerates /robots.txt
manifest.tssrc/app/manifest.tsPWA manifest
opengraph-image.tsxsrc/app/blog/[slug]/opengraph-image.tsxPer-route OG card
icon.svg, favicon.icosrc/app/Discovered by filename, not imported
globals.csssrc/app/globals.cssImported once by the root layout

One of those has a trap worth the paragraph. opengraph-image.tsx under a dynamic segment needs its own generateStaticParams. It does not inherit the one in the sibling page.tsx, and if you omit it the card is generated per request instead of at build time — the route still works, which is exactly why the regression is easy to ship. The file that burned us is src/app/blog/[slug]/opengraph-image.tsx; it now exports its own copy.

The second trap is a rename. The request-level hook that was src/middleware.ts through Next 13–15 is src/proxy.ts in Next 16, exporting proxy() rather than middleware():

// src/proxy.ts — sibling of app/, NEVER inside it
export async function proxy(request: NextRequest) {
  let response = NextResponse.next({ request });
  // ...refresh the session, write rotated cookies onto the response
  return response;
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\.(?:svg|png|jpg|jpeg|webp|gif|ico|webmanifest|html)$).*)",
  ],
};

Both names live at the src/ root, beside app/, never within it. Put it inside app/ and it silently never runs. The Next.js 16 proxy and middleware post covers the rename in full.

Why no components in app/

Because "where does this file go?" has to be answerable without opening the file. A components tree with declared layers answers it; colocation puts the answer in whoever's head wrote it.

The layering here is Atomic Design, and the part that matters is not the names, it is the single permitted import direction:

pages (app/) → templates → organisms → molecules → atoms
                               │            │
                               └────────────┴──→ data (src/data) + tokens (globals.css)

A level may import from the levels below it. Never sideways within a level, never upward. Two consequences fall out of that one rule:

  • An organism owns one page section and its own <section> shell, maps over one data array, and composes molecules. 33 of them, one per section.
  • Molecules never import runtime values from data files — type-only imports are fine. The organism computes and passes props down. A molecule that reaches into src/data is a molecule you cannot reuse, and it is the single most common drift in this tree.

With that rule in place, app/page.tsx is three lines: render <HomeTemplate />. The template orders organisms and holds no content. Nothing in app/ knows what a feature card looks like.

For the narrower question of where one component file should sit, where to put components in Next.js walks the decision, and Atomic Design in Next.js is the layering in detail.

src/ or no src/

Use it. The official position is that either is fine and it is "largely a matter of preference," which is true for a five-route project and stops being true once the repository root fills up with things that are not application code.

The root here holds public/, scripts/ (four tool directories: audit, images, release, seo), supabase/migrations/, docs/, and six config files. None of that is application source. With src/, the boundary is visible in one ls; without it, app/ sits in a directory of 12 siblings and the reader has to know which ones matter.

The cost is one line of config, already written:

// tsconfig.json
"paths": { "@/*": ["./src/*"] }

Then every import is absolute and survives a file move: @/components/molecules/BlogCard, @/data/catalog, @/lib/entitlements. Relative imports out of a nested route (../../../components) are what makes a tree unmovable, and the alias is the cheapest thing in this entire post.

Route groups: this tree has none

Zero (parentheses) folders across 34 directories, and that is a deliberate reading of what they are for. A route group changes nothing about the URL — it exists to give a subtree its own layout or to split one. The two subtrees here that could want one do not need it:

  • The 7 pSEO hubs (nextjs-admin-template, tailwind-shop-template, …) are flat top-level routes sharing the root layout. Grouping them would add a directory and change nothing.
  • The dashboard already has its own layout at src/app/dashboard/layout.tsx, which is a real route segment. It did not need a group to get one.

Reach for a group when you want a layout boundary that is not a URL boundary — a marketing shell and an app shell under the same domain is the canonical case. Reach for it to "organise" and you have added a layer of indirection in exchange for tidiness in a file tree you look at, not one the framework reads. Next.js route groups has the cases where it earns its keep.

Where everything that is not a route or a component goes

DirectoryWhat lives thereWhy not in app/
src/data20 typed arrays — section copy, catalog.ts (111 products), blog.ts (337 post entries)Content is imported by many routes; it belongs to none of them
src/content/blog337 .mdx article bodiesCompiled at build time by an explicit slug → import() map in src/lib/blog.ts
src/libClients, the entitlement engine, download authorization, 31 test filesLogic with no JSX has no reason to sit in a route folder
scripts/Release, screenshot, SEO and audit toolingNot shipped. Keeping it out of src/ keeps it out of the bundle graph
supabase/migrations8 SQL files, 271 linesThe schema is not application code

Tests sit next to the code they test, in __tests__ directories: src/lib/__tests__/entitlements.test.ts, src/data/__tests__/catalog.test.ts. 31 of them. A top-level tests/ mirror would be a second tree to keep in sync with the first, and it always drifts in the same direction — the mirror gets stale.

The part that keeps the structure true

A folder convention that is only written down is a folder convention that decays. The invariants here are executable, which is the difference between a structure and a suggestion:

  • npm test fails if a blog post's three files (registry entry, .mdx body, loader) disagree — the exact drift a wildcard dynamic import would have hidden until runtime.
  • npm test fails if a catalog product references an image file that does not exist under public/.
  • npm run build type-checks every component, so a structural mistake fails the build rather than the page.

That last one has a specific bite worth knowing: lowercase SVG element names in JSX (<clippath> instead of <clipPath>) compile happily in dev and fail next build. Dev-server-only verification of a tree this size is not verification.

Troubleshooting

SymptomCauseFix
Folder exists, URL 404sNo page.tsx or route.ts in itAdd one — a segment is not public without it
middleware.ts never runsNext 16 renamed it, or it is inside app/Rename to src/proxy.ts, export proxy(), keep it beside app/
OG image regenerates per requestDynamic-segment opengraph-image.tsx missing generateStaticParamsExport its own copy; it does not inherit the page's
Hydration error in the root layout<html>/<body> missing or duplicatedOnly the root layout and global-error.tsx declare them
@/ import unresolvedpaths missing, or baseUrl/include wrong"@/*": ["./src/*"] in tsconfig.json
Route group changed the URLA (folder) renamed with a typo, or nested page.tsx movedParentheses are stripped from the path; check the segment that holds the page.tsx
New Tailwind utility silently missing@theme tokens edited while the dev server ranRestart npm run dev — Tailwind v4 + Turbopack does not hot-reload @theme
Component imports a sibling at the same layerLayering rule brokenLift the shared part down a layer, or pass it as a prop

Frequently asked questions

Is the Next.js 13 file structure still current? The App Router conventions in this post are unchanged through Next 16 — folders are routes, page/route makes a segment public, the special filenames are the same. One thing moved: middleware.ts became proxy.ts. Anything written for Next 13's app/ still applies; anything written for pages/ is a different router.

Should components go in app/ or outside it? Outside, once you have more than a handful. The framework allows colocation and it is genuinely convenient for a one-off component used by exactly one route. The failure mode is gradual: colocated components get imported by a second route, then a third, and the folder that was "this route's components" becomes a shared directory with a misleading name. Our 92 components are all in src/components, and no route owns any of them.

How deep should route nesting go? As deep as the URL you want, and no deeper. Nesting in app/ is URL structure, not organisation — every folder you add is a path segment unless you wrap it in parentheses. This tree's deepest route is three segments (app/api/webhooks/lemonsqueezy/route.ts) because that is the URL the payment provider posts to.

Do I need src/? Not for correctness — only for the boundary. If your repository root is app/, public/ and three config files, skip it. If it has a scripts directory, a docs directory, a migrations directory and tooling config, src/ is what tells a newcomer where the application stops.

Templates in this post

ASoc Vault (a fintech SaaS marketing site with unified accounts, FX and tiered pricing), ASoc Vox (an AI-voiceover site with synthesis, cloning and 3-tier pricing) and ASoc Weave (an AI website-builder SaaS site) all ship on the tree above — src/app for routes, four component layers beside it, section copy in src/data — so the structure is readable before you change a line of it.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Tutorial11 min read

Next.js 16 Renamed Middleware to Proxy: What Changes and What Breaks

Renaming the file is a third of the migration. The exported function has to change too, keeping both files is a build error, and the proxy runs on Node rather than the Edge.

Read more
Tutorial11 min read

A/B Testing in Next.js 16: The Proxy Recipe Costs the CDN, Not SSG

Rewriting in middleware does not make your pages dynamic — both variants stay prerendered. What it really costs is the shared cache and every request's critical path.

Read more
Tutorial11 min read

An Accessible Mega Menu in Next.js Without a Headless UI Library

It is a navigation landmark, not an application menu — and the ARIA menu pattern most tutorials copy removes your nav from every screen reader's link list. Six behaviours, eighty lines.

Read more