35% OFF
Ends14d 00:00:00
Shop
Back to Blog
nextjstypescriptreactjavascript

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

Pixel Anas··5 min read

The exact folder structure I use building production Next.js 15 apps — route groups, server components, and how to organize code that actually scales.

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