How to Structure a Next.js App Router Project That Stays Maintainable in 2026
In a recent audit of 200 Next.js codebases, teams reported that over 60 % spent more time fixing import errors and hunting down duplicated logic than actually shipping features. The root cause wasn’t lack of talent or missing tests; it was a folder structure that grew organically without any guiding principles. When the App Router arrived, many teams simply dropped their old pages/ folder into app/ and hoped for the best, only to discover that server‑client boundaries, route‑level layouts, and server actions introduced new layers of complexity.
What if you could start with a structure that deliberately separates concerns, makes the default rendering mode server‑first, and scales linearly with the number of engineers? Imagine onboarding a new hire and having them point to a feature folder and instantly understand where the UI, the data‑fetching logic, and the route handlers live. That is not a fantasy; it is a proven pattern used by high‑velocity teams building production‑grade MVPs in under 30 days.
In this guide you will learn a concrete, battle‑tested folder layout for Next.js 15 App Router projects that stays maintainable as your product evolves from prototype to enterprise scale. We’ll cover why the src directory matters, how to organize by feature rather than by route, the server‑first mindset that eliminates unnecessary hydration, and where to place shared utilities, API wrappers, and type definitions. Each section includes concrete examples, a comparison table, and links to free tools that automate the boring parts.
By the end you will have a checklist you can apply to any new Next.js project today, and a set of refactoring steps you can apply to an existing codebase without a massive rewrite. Let’s dive in.
TL;DR — Key Takeaways
- Start every Next.js project with a src/ root to keep configuration files clean.
- Use the App Router exclusively; treat server components as the default.
- Group code by business feature inside src/features, not by file type or route.
- Centralize utilities, API clients, and TypeScript types in src/lib.
- Adopt linting rules and barrel‑export conventions that prevent client‑only code from leaking into server components.
Why the src/app Foundation Sets the Stage for Long‑Term Health
Putting all source code inside a src/ directory is more than a convention; it creates a clear boundary between project configuration (package.json, next.config.js, tsconfig.json) and the actual application logic. When your repository grows to include scripts, documentation, and infrastructure‑as‑code folders, having a dedicated src/ keeps the root readable and prevents accidental imports of config files into your client bundles.
Inside src, the app/ folder is where the App Router lives. Every subfolder under app/ becomes a URL segment, and each folder can export its own layout.js, loading.js, error.js, and route handlers. This co‑location of UI, data fetching, and error boundaries means that a developer can open a single folder and see everything needed to render that part of the site, drastically reducing context‑switching.
Contrast this with a flat structure where components, styles, and API calls are scattered across the root. In such layouts, a simple change to a header component might require editing files in three different directories, increasing the chance of merge conflicts and making it harder for newcomers to build a mental model.
To illustrate the difference, here is a comparison of three common folder strategies for a medium‑sized SaaS app:
| Strategy | Pros | Cons |
|---|---|---|
| Flat (components/, lib/, pages/ at root) | Very quick to set up for tiny projects | Hard to locate related files; scaling leads to duplicated UI and unclear ownership |
| Feature‑first inside src/ (src/features/) | Clear ownership, easy onboarding, isolates changes to a single folder | Requires discipline to avoid creating overly large feature folders |
| Route‑mirrored (src/app/ mirrors URL) | Intuitive for developers familiar with file‑based routing | Can scatter UI components across many route folders, making reuse difficult |
The table shows why a hybrid approach—feature‑first modules living under src/ while still using the App Router for routing—delivers the best of both worlds. You keep the routing intuition of app/ while gaining the modularity of feature folders.
Feature‑First Modules: Organize by Business Capability, Not by File Type
The core idea is simple: every major piece of functionality—authentication, billing, dashboard, settings—gets its own folder under src/features. Inside each feature folder you place everything that feature needs: React components (both server and client), custom hooks, route handlers (if the feature owns a page), styles, and even feature‑specific TypeScript types. This colocation reduces the need for long relative imports like ../../../components/Button and makes it obvious which files belong together.
For example, a billing feature might look like this:
src/
└── features/
└── billing/
├── components/
│ ├── BillingForm.server.tsx
│ └── UpgradeButton.client.tsx
├── hooks/
│ └── useSubscription.ts
├── lib/
│ └── stripeClient.ts
├── routes/
│ └── page.tsx
├── types.ts
└── index.ts
Notice that the route handler lives inside the feature folder rather than being scattered in src/app. You can still expose the route to the App Router by creating a thin wrapper in src/app/billing/page.tsx that simply re‑exports the feature’s route handler:
// src/app/billing/page.tsx
export { default } from '@/features/billing/routes/page';
This indirection keeps the App Router’s file‑system routing intact while preserving the feature‑first mental model.
When a feature grows large enough to warrant sub‑features (e.g., billing/invoices and billing/subscriptions), you can nest folders accordingly. The key is to stop splitting by technical concerns (components vs hooks vs utils) and start splitting by what the user actually does.
Internal tooling can help keep this structure tidy. The JSON to TypeScript Converter at /tools/json-to-typescript lets you generate exact TypeScript interfaces from your backend’s API responses, ensuring the types inside each feature folder stay in sync without manual copying.
Server‑First Mindset: Make Server Components the Default
One of the most common sources of unnecessary JavaScript in Next.js apps is the over‑use of ‘use client’ directives. When every component is marked as a client component, you lose the biggest performance advantage of the App Router: the ability to render HTML on the server and send zero‑JavaScript HTML to the browser for static sections.
The rule of thumb is straightforward: start every component as a server component. Only add 'use client' when you need to access browser‑only APIs (window, document, localStorage, Canvas) or when you need to use React hooks that rely on client‑side state such as useState, useEffect, or useReducer. If a component merely transforms props into JSX, keep it server‑side.
Consider a product card that displays a title, price, and an “Add to cart” button. The card itself can be a server component because it only reads props. The button, however, needs to manage local UI state (optimistic update) and may call a client‑only cart API, so it becomes a client component:
// src/features/product/components/ProductCard.server.tsx
export default function ProductCard({ product }: { product: Product }) {
return (
{product.title}
${product.price}
);
}
// src/features/product/components/AddToCartButton.client.tsx
'use client';
import { useState } from 'react';
import { addToCart } from '@/features/product/lib/cartApi';
export default function AddToCartButton({ productId }: { productId: string }) {
const [pending, setPending] = useState(false);
async function handleClick() {
setPending(true);
try {
await addToCart(productId);
} finally {
setPending(false);
}
}
return (
);
}
By keeping the bulk of the UI in server components, you reduce the amount of JavaScript sent to the browser, improve First Contentful Paint, and lower the hydration cost. Teams that adopt this rule consistently report a 30‑40 % reduction in bundle size for typical e‑commerce pages.
To enforce this convention automatically, add an ESLint rule that flags any 'use client' directive inside a folder named server or inside a component that does not import any client‑only hooks. Many teams share this rule via an eslint-plugin-next-server-first package, which you can install from npm.
Centralizing Data‑Access, API Wrappers, and Shared Types
Even with feature‑first modules, certain logic is genuinely shared across the application: authentication helpers, logging utilities, database clients, and API request wrappers. Placing these in a src/lib folder keeps them importable from any feature without creating circular dependencies.
A typical lib folder might contain:
- src/lib/auth.ts – a thin wrapper around next-auth (or Clerk) that provides a getServerSession helper for server components and a useSession hook for client components.
- src/lib/fetcher.ts – a reusable fetch wrapper that automatically attaches the current user’s JWT, handles retry logic, and throws typed errors.
- src/lib/db.ts – a Prisma client instance configured with connection pooling.
- src/lib/types.ts – global TypeScript interfaces that are truly shared (e.g., User, Session, PaginatedResponse). Feature‑specific types stay inside the feature folder.
- src/lib/logger.ts – a wrapper around winston or pino that adds request IDs and environment context.
Here’s a concrete example of a fetcher that reads the session from a server component and injects the token:
// src/lib/fetcher.ts
export async function fetcher(endpoint: string, init?: RequestInit): Promise {
const session = await getAuthSession(); // from src/lib/auth.ts
const headers = new Headers(init?.headers);
if (session?.accessToken) {
headers.set('Authorization', `Bearer ${session.accessToken}`);
}
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${endpoint}`, {
...init,
headers,
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.message ?? `HTTP ${res.status}`);
}
return res.json();
}
Because the fetcher lives in lib/, any feature can import it without worrying about where the authentication logic lives. This also makes it easier to swap out the auth provider (e.g., moving from next-auth to Clerk) by editing only src/lib/auth.ts.
When you need to generate TypeScript types from your backend’s OpenAPI schema or from example JSON responses, the JSON to TypeScript Converter tool at /tools/json-to-typescript is invaluable. Run it as part of your CI pipeline to keep src/lib/types.ts up to date automatically.
In addition to raw utilities, consider placing feature flags in src/lib/flags.ts. A simple flag system lets you wrap risky code in a condition that can be flipped at runtime without a deploy. The article “Feature Flags 101: Ship Faster Without Breaking Production” at /blog/feature-flags-101-ship-faster-without-breaking-production explains how to implement this pattern with minimal overhead.
Scaling with Conventions: Linting, Barrel Exports, and Testing
Even the best folder structure will erode if the team does not enforce lightweight conventions. A combination of ESLint rules, barrel‑export guidelines, and testing strategies keeps the codebase clean as it scales.
First, enable the built-in Next.js ESLint plugin and add a custom rule that disallows relative imports that climb more than two levels up (e.g., ../../../). This encourages developers to keep imports within the same feature or to use the lib/ alias. You can also add a rule that forbids 'use client' in any file whose path contains server or lib, reinforcing the server‑first mindset.
Second, be judicious with barrel exports (index.ts files that re‑export everything from a folder). While they can tidy up imports, they also obscure which symbols are actually being used and can cause unintended side effects if a file exports a component with heavy client‑side dependencies. Limit barrels to folders that contain only pure utilities or types, and avoid them in UI component folders.
Third, adopt a testing strategy that mirrors your folder layout. Place unit tests next to the file they test (e.g., components/Button.test.tsx) and integration tests in a tests/ folder that mirrors the feature hierarchy. This makes it trivial for a developer to locate the test for a given component and encourages test‑driven development when adding new features.
Finally, document the structure in a CONTRIBUTING.md file that includes the folder diagram, the server‑first rule, and links to the internal tools you rely on (JSON to TypeScript Converter, MVP Prioritizer, etc.). When new engineers join, they can read this document and start contributing within hours instead of days.
By combining these conventions with the feature‑first, server‑first, and lib‑centralized patterns described earlier, you create a self‑reinforcing system: the folder layout guides where code belongs, the linting rules prevent common mistakes, and the tooling reduces boilerplate. The result is a Next.js App Router project that remains understandable, performant, and easy to evolve—whether you are shipping your first MVP or preparing for a Series A.
Where to Go From Here
Now that you have a concrete blueprint for a maintainable Next.js App Router project, the next step is to apply it to your next feature branch. Start by creating a src/ folder if you don’t already have one, move your existing app/ directory inside it, and then reorganize your code into feature folders under src/features. Run the linting rules you’ve added and fix any 'use client' violations that appear in server‑only sections.
If you are working on an existing codebase that still uses the Pages Router, consider a gradual migration: create a new src/app/ folder for new features while keeping the old pages/ untouched, and use the Next.js middleware to route legacy paths to the new App Router when possible. Over time, you can retire the pages/ folder entirely.
Remember that architecture is a tool, not a goal. The structure described here exists to help you ship value faster, reduce cognitive load, and keep technical debt from compounding. At HYVO, we help teams turn high‑level visions into battle‑tested, scalable products without getting lost in architectural debates—because we know that the best architecture is the one that lets you focus on building what matters.
If you’d like to see how we apply these principles in practice, feel free to reach out. We specialize in building production‑grade MVPs in under 30 days, handling everything from complex fintech ledgers to AI‑integrated platforms, and we’d be happy to discuss how we can help you achieve the same.
Frequently Asked Questions
What is the recommended folder structure for a Next.js 15 App Router project?
Start with a src directory at the project root. Inside src, keep an app folder for routes, a lib folder for utilities and shared types, a components folder for reusable UI, and a features folder where each business capability gets its own subfolder containing its components, hooks, and route handlers. This layout keeps concerns separated and scales well as the team grows.
Software we build and run
Five products, operated by the same team that writes here.
Hyvo CRM
AI-native CRM
The CRM that explains itself.
Hyvo Campus
Complete school management
The whole school, in one place.
Hyvo Concierge
AI concierge for your website
Answers with proof. Acts, not just chats.
Hyvo Cloud
Cloud cost optimization
Finds the money. Fixes it too.
Hyvo Guard
AI governance
Shadow AI, found. Policy, enforced.
See all productsBook a demo