DEV Community

Anas Sheikh
Anas Sheikh

Posted on

The Next.js 15 Folder Structure I Use in Every Production Project

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

/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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
// 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(),
});
Enter fullscreen mode Exit fullscreen mode

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' },
  ],
}
Enter fullscreen mode Exit fullscreen mode

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)