DEV Community

Krunal Panchal
Krunal Panchal

Posted on

The Next.js 15 App Router Project Structure That Scales (With Examples)

Every Next.js 15 project starts clean. Six months later, half of them are a mess of components dumped in /app, utils folders no one understands, and server/client logic mixed randomly. Here's the structure we use after building 50+ production Next.js apps.

The Core Problem with App Router Projects

The App Router's file-system routing is powerful but opinionated in exactly one way: how routes map to files. Everything else — component organization, data fetching patterns, shared logic, server vs client boundaries — is up to you.

Most teams discover their mistakes at scale, not during setup. This post skips the discovery phase.


The Structure

my-app/
├── app/                          # Routes only — no logic here
│   ├── (marketing)/              # Route group: public pages
│   │   ├── page.tsx
│   │   └── about/page.tsx
│   ├── (dashboard)/              # Route group: authenticated app
│   │   ├── layout.tsx            # Auth check here
│   │   ├── dashboard/page.tsx
│   │   └── settings/page.tsx
│   ├── api/                      # API routes
│   │   └── [...]/route.ts
│   ├── layout.tsx                # Root layout
│   └── globals.css
│
├── components/                   # Shared UI components
│   ├── ui/                       # Primitives (Button, Input, etc.)
│   ├── forms/                    # Form components
│   └── layouts/                  # Page layout shells
│
├── features/                     # Feature modules (the key pattern)
│   ├── auth/
│   │   ├── components/           # Auth-specific UI
│   │   ├── hooks/                # useAuth, useSession
│   │   ├── actions.ts            # Server actions for auth
│   │   └── types.ts
│   ├── billing/
│   │   ├── components/
│   │   ├── actions.ts
│   │   └── types.ts
│   └── dashboard/
│       ├── components/
│       ├── hooks/
│       └── actions.ts
│
├── lib/                          # Infrastructure / integrations
│   ├── db/                       # Prisma client + queries
│   │   ├── client.ts
│   │   └── queries/
│   ├── auth/                     # Clerk/NextAuth config
│   ├── stripe/                   # Stripe client + webhooks
│   └── email/                    # Resend/email templates
│
├── hooks/                        # Global client-side hooks
├── types/                        # Global TypeScript types
├── utils/                        # Pure utility functions
└── config/                       # App configuration
    ├── site.ts                   # Site metadata
    └── nav.ts                    # Navigation config
Enter fullscreen mode Exit fullscreen mode

The Rules Behind the Structure

1. App directory = routes only

No business logic in app/. No data fetching in page components beyond calling a function from features/ or lib/. The page file should be readable in 30 seconds.

// app/(dashboard)/dashboard/page.tsx — good
import { getDashboardData } from '@/features/dashboard/actions'
import { DashboardView } from '@/features/dashboard/components/DashboardView'

export default async function DashboardPage() {
  const data = await getDashboardData()
  return <DashboardView data={data} />
}
Enter fullscreen mode Exit fullscreen mode

2. Features over shared components

The mistake: putting everything in /components. You end up with a 40-file flat list where UserCard.tsx is next to PricingTable.tsx and nobody knows what's shared vs feature-specific.

The fix: feature modules. Auth-related UI lives in features/auth/components/. It can only be imported by auth routes and the root layout. Dashboard components live in features/dashboard/. If something is used in two features, it graduates to /components.

3. Server actions are the API layer

For most Next.js apps, you don't need separate API routes for your own data. Server actions in features/*/actions.ts replace the traditional API route pattern for form submissions, mutations, and authenticated data fetching.

// features/billing/actions.ts
'use server'
import { auth } from '@/lib/auth'
import { stripe } from '@/lib/stripe'

export async function createCheckoutSession(priceId: string) {
  const { userId } = await auth()
  // ... create session
}
Enter fullscreen mode Exit fullscreen mode

Keep API routes (app/api/) for: webhooks (Stripe, Clerk), public endpoints (other services consuming your API), and file upload handlers.

4. Explicit server/client split

Every component is a Server Component by default. Add 'use client' only when you need:

  • useState / useEffect
  • Browser APIs
  • Event handlers
  • Context consumers

The boundary: pass data down from Server Components as props. Keep interactive islands small.

// features/dashboard/components/MetricsCard.tsx — Server Component (no directive)
export function MetricsCard({ value, label }: Props) {
  return <div>{label}: {value}</div>  // Static, no interactivity needed
}

// features/dashboard/components/MetricsChart.tsx — Client Component (needs recharts)
'use client'
import { LineChart } from 'recharts'
export function MetricsChart({ data }: Props) {
  return <LineChart data={data} />
}
Enter fullscreen mode Exit fullscreen mode

5. Route groups for auth boundaries

Use route groups (marketing) and (dashboard) to separate public and authenticated routes. Put auth checking in the group layout, not on individual pages.

// app/(dashboard)/layout.tsx
import { redirect } from 'next/navigation'
import { auth } from '@/lib/auth'

export default async function DashboardLayout({ children }) {
  const session = await auth()
  if (!session) redirect('/login')
  return <>{children}</>
}
Enter fullscreen mode Exit fullscreen mode

What Goes Where: Quick Reference

Code Goes in
Page-level UI app/*/page.tsx
Shared UI primitives components/ui/
Feature-specific UI features/[name]/components/
Server mutations features/[name]/actions.ts
Third-party clients lib/[service]/
Database queries lib/db/queries/
Client-side state hooks features/[name]/hooks/ or hooks/
TypeScript types features/[name]/types.ts or types/
Config/constants config/

The Scaling Test

Ask these questions about any file you create:

  1. Could a new team member find this without asking? If not, it's in the wrong place.
  2. If this feature is deleted, can I delete one folder and be done? If not, it's too coupled.
  3. Is this a Server or Client Component? If you're unsure, it should be a Server Component until you need client features.

Starter Template

We maintain a Next.js 15 full-stack project structure with this layout pre-wired — includes Prisma, Clerk auth, Stripe, Resend, and shadcn/ui. Clone and go.


What's tripping you up in your Next.js structure? Happy to answer specific questions.

Top comments (0)