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} />;
}
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');
}
// 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>
);
}
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
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)
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>
);
}
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();
}
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>
);
}
Key Takeaways
- Use App Router for all new projects — Pages Router is legacy
-
Server Components are the default — only add
'use client'when you must -
Server Actions replace most API routes — keep
api/minimal - Route groups organize without affecting URLs — separate marketing from dashboard
- 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)