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:
- https://neurodash-dashbord.vercel.app/
- https://og-ai-next.vercel.app/
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