Next.js 15 Project Structure Guide: How to Organize Code for Production in 2026
Every Next.js project structure guide from 2023 and earlier is outdated. Next.js 15 fundamentally changed how you organize code with App Router, Server Components, and Server Actions. Here's the structure that works for production apps in 2026.
The Problem: Your Old Next.js Structure Is Broken
If you're following a project structure guide from 2023 or earlier, stop. Four fundamental changes in Next.js have made those guides actively harmful for production applications:
When I say "harmful," I mean it literally. Teams that organize code using old patterns—especially importing from /pages/api routes in client components—create coupling that makes migration to App Router nearly impossible without a complete refactor. You'll discover this problem six months into a production application when you try to scale.
The good news: The correct structure is simpler than the old one. It eliminates entire folders you no longer need.
The Next.js 15 Folder Structure That Actually Works
Here's the structure for a production Next.js 15 application:
```
project-root/
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ │ └── page.tsx
│ │ ├── signup/
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── [team]/
│ │ ├── layout.tsx
│ │ └── settings/
│ │ └── page.tsx
│ ├── api/
│ │ ├── auth/
│ │ │ └── callback/
│ │ │ └── route.ts
│ │ └── webhooks/
│ │ └── stripe/
│ │ └── route.ts
│ ├── layout.tsx
│ └── page.tsx
├── lib/
│ ├── api/
│ │ ├── client.ts
│ │ ├── users.ts
│ │ └── teams.ts
│ ├── auth/
│ │ ├── session.ts
│ │ └── middleware.ts
│ ├── db/
│ │ └── queries.ts
│ └── utils.ts
├── components/
│ ├── ui/
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── dialog.tsx
│ ├── auth/
│ │ └── login-form.tsx
│ └── dashboard/
│ └── header.tsx
├── hooks/
│ ├── useAuth.ts
│ └── useTeams.ts
├── types/
│ └── index.ts
├── public/
├── .env.local
└── next.config.ts
```
The critical difference from older guides: No /pages directory. No /pages/api for data fetching. No separate API layer sitting in a different architectural zone.
Why This Structure Works for Server Components
The App Router assumes your code runs on the server by default. This is the mental shift that breaks people who learned Next.js before 2023.
In the old structure, you'd create an API endpoint at /pages/api/users.ts, then fetch it from a client component:
```
// Old pattern (broken in 2026)
// pages/api/users.ts - sits isolated from the rest of your app
export default async function handler(req, res) {
const users = await db.users.findAll();
res.status(200).json(users);
}
// app/dashboard/page.tsx - client component, must fetch via HTTP
'use client';
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
```
This forces HTTP round trips for something that could happen once on the server.
In Next.js 15, you put your database queries directly in the server component:
```
// app/(dashboard)/page.tsx - Server Component by default
import { getUsersWithTeams } from '@/lib/db/queries';
export default async function DashboardPage() {
const users = await getUsersWithTeams();
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
```
The queries live in /lib/db/queries.ts because that's where business logic belongs—not isolated in a separate /pages/api folder.
The /lib/api Pattern for Client-Side Data Fetching
You still need HTTP endpoints, but only for:
For everything else—data that client components need—use React Server Components and Server Actions.
When you do need a client-side API, structure it like this:
```
// lib/api/client.ts - single source of truth for all HTTP calls
const API_URL = process.env.NEXT_PUBLIC_API_URL || '';
export async function fetchUsers(teamId: string) {
const res = await fetch(`${API_URL}/api/teams/${teamId}/users`);
if (!res.ok) throw new Error('Failed to fetch users');
return res.json();
}
export async function updateUser(userId: string, data: Partial<User>) {
const res = await fetch(`${API_URL}/api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Failed to update user');
return res.json();
}
// Client component can now use this
'use client';
import { fetchUsers } from '@/lib/api/client';
export function UserList({ teamId }) {
const [users, setUsers] = useState([]);
useEffect(() => {
fetchUsers(teamId).then(setUsers);
}, [teamId]);
return ...
}
```
All HTTP calls flow through one module. This prevents coupling scattered throughout your codebase and makes it trivial to add logging, error handling, or authentication headers globally.
Server Actions Replace Mutations
In the old structure, you'd create POST endpoints for data mutations. In Next.js 15, you use Server Actions:
```
// lib/actions/users.ts
'use server';
export async function updateUserName(userId: string, name: string) {
const user = await db.users.update({
where: { id: userId },
data: { name },
});
revalidatePath(`/dashboard/${user.teamId}`);
return user;
}
// components/edit-user.tsx
'use client';
import { updateUserName } from '@/lib/actions/users';
export function EditUserForm({ user }) {
const [pending, setPending] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setPending(true);
const formData = new FormData(e.target);
const result = await updateUserName(user.id, formData.get('name'));
setPending(false);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" defaultValue={user.name} />
<button disabled={pending}>Save</button>
</form>
);
}
```
Server Actions are typed, no serialization required, and they automatically revalidate caches. They're the correct pattern for production applications.
Turbopack Compatibility: Aliases and Imports
Turbopack (Next.js 15's default bundler) handles aliases differently than Webpack. Your /tsconfig.json should include:
```
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
```
This lets you import from @/lib, @/components, etc. Turbopack respects these. Cold starts are under 1 second even on large projects, compared to 3-5 seconds with Webpack.
Avoid dynamic require() calls for plugin systems. Turbopack can't tree-shake them. Use static imports or build your plugin system differently.
Building This Correctly With AI Assistance
When you're using Claude Code or another AI assistant to scaffold a Next.js 15 project, make sure it follows these patterns. Many code generation templates still use the 2023 structure because they haven't been updated.
The structural differences matter enormously when you're building with AI assistance. If your AI assistant is generating /pages/api routes when you should be using Server Actions, it's creating technical debt that will cost weeks to refactor later. The same applies to project structure—get the folders right from day one.
ZipBuild automatically scaffolds Next.js 15 projects with this correct structure baked in, Server Actions configured, and /lib/api patterns set up. You skip the architectural decisions and start building immediately.
Key Takeaways
Your 2023 folder structure guide is wrong for production in 2026. Use this one instead.
Try the free discovery chat at zipbuild.dev to scaffold a Next.js 15 project with the correct structure already in place.
Written by ZipBuild Team
Ready to build with structure?
Try the free discovery chat and see how ZipBuild architects your idea.
Start Building