I've built multiple production Next.js apps.
Every time I started a new project I wasted hours debating folder structure.
Now I use the same structure every time. Here it is.
The Full Structure
src/
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ │ └── page.tsx
│ │ └── signup/
│ │ └── page.tsx
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── analytics/
│ │ │ └── page.tsx
│ │ └── settings/
│ │ └── page.tsx
│ ├── (marketing)/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api/
│ │ ├── auth/
│ │ │ └── route.ts
│ │ └── users/
│ │ └── route.ts
│ ├── layout.tsx
│ └── globals.css
│
├── components/
│ ├── ui/ ← reusable atoms (Button, Input, Badge)
│ ├── layout/ ← Navbar, Footer, Sidebar
│ └── sections/ ← page-level sections (Hero, Features)
│
├── lib/
│ ├── db.ts ← database client
│ ├── auth.ts ← auth helpers
│ ├── utils.ts ← shared utilities
│ └── validations.ts ← Zod schemas
│
├── hooks/ ← custom React hooks
├── types/ ← TypeScript interfaces
└── config/
└── site.ts ← site-wide config
Why Route Groups Are a Game Changer
The (auth), (dashboard), (marketing) folders are route groups — the parentheses mean they don't affect the URL.
This lets you have different layouts for different parts of your app:
// app/(dashboard)/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}
// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
return (
<div>
<Navbar />
{children}
<Footer />
</div>
);
}
/dashboard gets the sidebar layout. / gets the navbar layout. No prop drilling. No conditional rendering. Clean.
Components — 3 Folders Only
I only use 3 component folders:
components/
├── ui/ ← Button, Input, Badge, Modal
├── layout/ ← Navbar, Footer, Sidebar, MobileMenu
└── sections/ ← HeroSection, FeaturesSection, CTASection
ui/ — small reusable components with no business logic:
// components/ui/Badge.tsx
interface BadgeProps {
children: React.ReactNode;
variant?: 'default' | 'brand' | 'muted';
}
export function Badge({ children, variant = 'default' }: BadgeProps) {
const variants = {
default: 'bg-slate-700 text-slate-300',
brand: 'bg-brand-500/10 text-brand-400 border border-brand-500/20',
muted: 'bg-surface-700 text-slate-500',
};
return (
<span className={`px-2.5 py-1 text-xs rounded-lg ${variants[variant]}`}>
{children}
</span>
);
}
sections/ — full page sections that import from ui/:
// components/sections/HeroSection.tsx
import { Badge } from '@/components/ui/Badge';
export function HeroSection() {
return (
<section className="min-h-screen flex items-center">
<Badge variant="brand">Now available</Badge>
<h1>Your headline here</h1>
</section>
);
}
The lib/ Folder
This is where I put everything that isn't a component:
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
}).format(date);
}
export function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
}
// lib/validations.ts — all Zod schemas in one place
import { z } from 'zod';
export const LoginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password too short'),
});
export const CreateUserSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
});
The config/ Folder
One file for all site-wide settings:
// config/site.ts
export const siteConfig = {
name: 'Your SaaS',
description: 'Your product description',
url: 'https://yoursaas.com',
links: {
twitter: 'https://x.com/yourhandle',
github: 'https://github.com/yourhandle',
},
nav: [
{ label: 'Features', href: '/features' },
{ label: 'Pricing', href: '/pricing' },
{ label: 'Blog', href: '/blog' },
],
}
Import it anywhere — no hardcoded strings scattered across files.
The Rules I Follow
1. Server components by default
Only add 'use client' when you actually need browser APIs or state.
2. Push client state down
Don't make a whole page client just for one interactive button. Extract the button into its own client component.
3. Co-locate what changes together
If a component, its types, and its hooks always change together — keep them in the same folder.
4. No barrel files in app/
Don't create index.ts files inside the app/ directory. Next.js routing depends on the file structure — barrel files confuse it.
Summary
| Folder | Purpose |
|---|---|
app/(groups)/ |
Separate layouts without affecting URLs |
components/ui/ |
Reusable atoms |
components/layout/ |
Structural components |
components/sections/ |
Page-level sections |
lib/ |
Utilities, DB, auth, validation |
hooks/ |
Custom React hooks |
types/ |
TypeScript interfaces |
config/ |
Site-wide configuration |
I use this exact structure in all my Next.js templates.
See it in a real codebase:
Get the templates: https://pixelanas.gumroad.com
What does your folder structure look like? Drop it below 👇
Anas — full-stack Next.js developer building SaaS products and premium templates. X: @pixelanas
Top comments (0)