DEV Community

Krunal Panchal
Krunal Panchal

Posted on • Originally published at groovyweb.co

How to Structure a Full-Stack Next.js 15 Project in 2026

Next.js 15 shipped major changes: App Router is the default, Server Components are the norm, Server Actions replace most API routes, and Turbopack changes the build story. Most project structure guides are outdated. Here's what actually works in production.

What Changed in Next.js 15

App Router Is the Default

The pages/ directory still works but app/ is where new features land. Parallel routes, intercepting routes, route groups, and streaming — all App Router only. Start new projects with app/.

Server Components Eliminate Most API Routes

In Next.js 14+, components are Server Components by default. They fetch data directly — no API route, no client-side fetch, no loading state boilerplate:

// app/dashboard/page.tsx — this runs on the server
async function DashboardPage() {
  const metrics = await db.query('SELECT * FROM metrics WHERE date = $1', [today]);
  return <MetricsGrid data={metrics} />;
}
Enter fullscreen mode Exit fullscreen mode

Server Actions Replace Form Handling

Forms no longer need API routes or client-side mutation libraries:

// app/actions/leads.ts
'use server';

export async function createLead(formData: FormData) {
  const email = formData.get('email') as string;
  await db.insert('leads', { email, source: 'website' });
  revalidatePath('/dashboard');
}
Enter fullscreen mode Exit fullscreen mode
// app/contact/page.tsx
import { createLead } from '@/app/actions/leads';

export default function ContactPage() {
  return (
    <form action={createLead}>
      <input name="email" type="email" required />
      <button type="submit">Get in Touch</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Production Folder Structure

After shipping 50+ Next.js 15 projects, this is the structure that scales:

src/
├── app/                    # App Router (routes + layouts)
│   ├── (marketing)/        # Route group: public pages
│   │   ├── page.tsx        # Homepage
│   │   ├── blog/
│   │   └── pricing/
│   ├── (dashboard)/        # Route group: authenticated pages
│   │   ├── layout.tsx      # Auth check + sidebar
│   │   ├── dashboard/
│   │   └── settings/
│   ├── api/                # Only for webhooks + external integrations
│   │   └── webhooks/
│   ├── actions/            # Server Actions
│   └── layout.tsx          # Root layout
├── components/
│   ├── ui/                 # Primitives (Button, Input, Card)
│   └── features/           # Domain components (LeadForm, PricingTable)
├── lib/
│   ├── db.ts               # Database client
│   ├── auth.ts             # Auth utilities
│   └── ai.ts               # AI/LLM client
├── hooks/                  # Client-side hooks only
└── types/                  # Shared TypeScript types
Enter fullscreen mode Exit fullscreen mode

Why Route Groups Matter

(marketing) and (dashboard) create separate layout trees without affecting URLs. Marketing pages get a minimal layout with nav + footer. Dashboard pages get auth middleware + sidebar. No URL prefix, clean separation.

The api/ Directory Is Tiny Now

With Server Actions handling mutations and Server Components handling reads, api/ routes are only needed for:

  • Webhooks (Stripe, GitHub, etc.)
  • External service callbacks
  • Third-party integrations that need a stable URL

Everything else? Server Actions.

Server vs Client Components: The Decision Rule

One-line rule: Default to Server Component. Add 'use client' only when you need browser APIs, event handlers, or useState/useEffect.

Server Component (default):
✅ Database queries
✅ File system access
✅ Environment variables
✅ Heavy computation
✅ Rendering static/dynamic content

Client Component ('use client'):
✅ onClick, onChange, onSubmit handlers
✅ useState, useEffect, useRef
✅ Browser APIs (localStorage, geolocation)
✅ Third-party client libraries (maps, charts)
Enter fullscreen mode Exit fullscreen mode

The mistake teams make: wrapping entire pages in 'use client' because one small section needs interactivity. Instead, keep the page as a Server Component and extract only the interactive part:

// app/pricing/page.tsx — Server Component
import { PricingToggle } from '@/components/features/PricingToggle';

export default async function PricingPage() {
  const plans = await db.query('SELECT * FROM plans');
  return (
    <main>
      <h1>Pricing</h1>
      {/* Only this component is a Client Component */}
      <PricingToggle plans={plans} />
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

AI Patterns in Next.js 15

Streaming AI Responses

// app/actions/chat.ts
'use server';

import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

export async function chat(message: string) {
  const result = await streamText({
    model: openai('gpt-4o'),
    messages: [{ role: 'user', content: message }],
  });
  return result.toDataStreamResponse();
}
Enter fullscreen mode Exit fullscreen mode

RAG with Server Components

// app/docs/[slug]/page.tsx — Server Component with RAG
export default async function DocPage({ params }: { params: { slug: string } }) {
  const doc = await db.query('SELECT * FROM docs WHERE slug = $1', [params.slug]);
  const related = await vectorSearch(doc.embedding, { limit: 5 });

  return (
    <article>
      <h1>{doc.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: doc.content }} />
      <RelatedDocs docs={related} />
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Use App Router for all new projects — Pages Router is legacy
  2. Server Components are the default — only add 'use client' when you must
  3. Server Actions replace most API routes — keep api/ minimal
  4. Route groups organize without affecting URLs — separate marketing from dashboard
  5. Keep AI logic in Server Components/Actions — never expose API keys to the client

The teams shipping fastest with Next.js 15 embrace the server-first model. Fight the urge to 'use client' everything.


Originally published at groovyweb.co

Top comments (0)