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.
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:
| Directory | Files | What it is allowed to contain |
|---|---|---|
src/app | 44 | page.tsx, route.ts, layout.tsx, the error/metadata specials, three assets |
src/components | 92 | Components, nothing else — no data, no fetching helpers |
src/data | 20 | Typed arrays. The only place section copy lives |
src/lib | 68 | Everything 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:
| File | Real path here | Job |
|---|---|---|
layout.tsx | src/app/layout.tsx | The root layout — it must declare <html> and <body>; Next does not add them |
page.tsx | src/app/page.tsx | Makes a segment a public route. 27 of these |
route.ts | src/app/api/download/route.ts | A handler instead of a page. 4 of these |
not-found.tsx | src/app/not-found.tsx | Branded 404 |
error.tsx | src/app/error.tsx | Client error boundary for the segment |
global-error.tsx | src/app/global-error.tsx | The root boundary — replaces the layout, so it needs its own <html> |
sitemap.ts | src/app/sitemap.ts | Generates /sitemap.xml |
robots.ts | src/app/robots.ts | Generates /robots.txt |
manifest.ts | src/app/manifest.ts | PWA manifest |
opengraph-image.tsx | src/app/blog/[slug]/opengraph-image.tsx | Per-route OG card |
icon.svg, favicon.ico | src/app/ | Discovered by filename, not imported |
globals.css | src/app/globals.css | Imported 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/datais 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
| Directory | What lives there | Why not in app/ |
|---|---|---|
src/data | 20 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/blog | 337 .mdx article bodies | Compiled at build time by an explicit slug → import() map in src/lib/blog.ts |
src/lib | Clients, the entitlement engine, download authorization, 31 test files | Logic with no JSX has no reason to sit in a route folder |
scripts/ | Release, screenshot, SEO and audit tooling | Not shipped. Keeping it out of src/ keeps it out of the bundle graph |
supabase/migrations | 8 SQL files, 271 lines | The 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 testfails if a blog post's three files (registry entry,.mdxbody, loader) disagree — the exact drift a wildcard dynamic import would have hidden until runtime.npm testfails if a catalog product references an image file that does not exist underpublic/.npm run buildtype-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
| Symptom | Cause | Fix |
|---|---|---|
| Folder exists, URL 404s | No page.tsx or route.ts in it | Add one — a segment is not public without it |
middleware.ts never runs | Next 16 renamed it, or it is inside app/ | Rename to src/proxy.ts, export proxy(), keep it beside app/ |
| OG image regenerates per request | Dynamic-segment opengraph-image.tsx missing generateStaticParams | Export its own copy; it does not inherit the page's |
| Hydration error in the root layout | <html>/<body> missing or duplicated | Only the root layout and global-error.tsx declare them |
@/ import unresolved | paths missing, or baseUrl/include wrong | "@/*": ["./src/*"] in tsconfig.json |
| Route group changed the URL | A (folder) renamed with a typo, or nested page.tsx moved | Parentheses 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 ran | Restart npm run dev — Tailwind v4 + Turbopack does not hot-reload @theme |
| Component imports a sibling at the same layer | Layering rule broken | Lift 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.
