The Next.js Folder Structure I Actually Use
May 2026 5 min read

After shipping StoryXen, Dealmate, and a fintech CRM — and refactoring each of them at least once — this is the structure I keep coming back to.
Not because I read it somewhere. Because I kept making the same mistakes and eventually got tired of them.
What the mess actually looks like
Early on I'd dump everything in app/. Data fetching in page components. Business logic next to layouts. A components/ folder that had both a generic Button and a UserDashboardStatsCard sitting side by side like that was fine.
It works until it doesn't. Then you're scared to touch anything because you don't know what breaks what.
The fix isn't a better folder name. It's a rule: every file has one job. Once I started asking "what is this file's single responsibility" before creating anything, the structure started writing itself.
The structure
my-next-app/
├── app/ // ROUTING LAYER — keep logic minimal here
│ ├── (auth)/
│ │ ├── login/
│ │ │ └── page.tsx
│ │ └── register/
│ │ └── page.tsx
│ ├── (dashboard)/
│ │ ├── layout.tsx // Sidebar + auth guard
│ │ └── page.tsx
│ ├── api/
│ │ └── webhook/
│ │ └── route.ts
│ ├── layout.tsx // Root layout — fonts, providers, metadata
│ ├── globals.css
│ └── error.tsx
│
├── features/ // DOMAIN LAYER — actual logic lives here
│ ├── auth/
│ │ ├── components/ // LoginForm, RegisterForm
│ │ ├── actions.ts // Server Actions: login, logout, register
│ │ └── schemas.ts // Zod validation
│ ├── dashboard/
│ │ ├── components/ // StatsCard, RecentActivity
│ │ └── queries.ts // Data fetching (cached)
│ └── users/
│ ├── components/ // UserTable, UserAvatar
│ ├── actions.ts // create, update, delete
│ └── queries.ts // getUserById, listUsers
│
├── components/ // SHARED UI — dumb and reusable only
│ ├── ui/ // Shadcn / Radix primitives
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ ├── modal.tsx
│ │ └── toast.tsx
│ └── layout/
│ ├── Navbar.tsx
│ ├── Sidebar.tsx
│ └── Footer.tsx
│
├── lib/ // INFRASTRUCTURE — server-only
│ ├── db.ts // Drizzle / Supabase client
│ ├── auth.ts // Auth config + session helpers
│ ├── api.ts // Typed fetch wrappers
│ ├── utils.ts // cn(), formatDate(), slugify()
│ └── safe-action.ts // Type-safe Server Action wrapper
│
├── hooks/ // Shared across 2+ features
│ ├── useAuth.ts
│ └── useDebounce.ts
│
├── types/
│ ├── index.ts // Re-exports everything
│ └── api.types.ts
│
├── constants/
│ ├── routes.ts // Typed URL constants — no magic strings
│ └── config.ts // Feature flags, app limits
│
├── prisma/
│ └── schema.prisma
│
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── .env.localThe rules I actually enforce
app/ is routing only. No queries, no logic, no server actions defined inline. Pages import from features/. That's it. The moment I wrote a fetch call directly in a page component on StoryXen, I regretted it two weeks later when I needed the same data somewhere else.
features/ owns its domain. Auth doesn't import from users. Users doesn't import from dashboard. If two features need something shared, it goes into hooks/ or lib/ — not a cross-import that creates invisible coupling you'll debug at 2am.
lib/ is server-only. Database client, auth config, fetch wrappers. Nothing here runs on the client. I learned this one the hard way when a Supabase client I initialized in a shared file started behaving differently depending on where it got imported.
components/ holds dumb UI. If a component knows what a "user" is, it doesn't belong here. Button, Input, Modal, Toast. Generic stuff that works anywhere. Anything domain-specific lives in features/{domain}/components/.
constants/routes.ts — this one I actually enforce on every project.
No hardcoded strings like "/dashboard" scattered across the codebase. One file, all routes, fully typed.
// constants/routes.ts
export const ROUTES = {
home: "/",
login: "/login",
register: "/register",
dashboard: "/dashboard",
} as const;Instead of:
router.push("/dashboard") // you will rename this route, you will forget to update thisYou do:
router.push(ROUTES.dashboard) // rename once, updates everywhereOn the Trusted Financing CRM alone there were maybe 15 places we were pushing to /dashboard as a raw string. That's 15 places to update when a route changes. routes.ts fixes this permanently.
The part nobody talks about
The structure doesn't save you. The discipline does.
I've seen clean folder structures with chaotic code inside them. The folders are just hints. What actually keeps a codebase sane is asking the same question every time you create a file: what is this file's one job?
If you can't answer that in a sentence, the file is doing too much.
That question — more than any folder layout — is what I actually try to carry project to project.