Next.js App Router Project Structure Best Practices (A Practical Blueprint for Product Teams)
Published 6/21/2026
Building a Next.js app is easy. Building one that a product team can actually maintain six months later? That’s a different story.
I’ve seen strong teams move fast at the start, then slow to a crawl because the project structure turned into a junk drawer. Pages are scattered. Components live in random folders. Server code mixes with UI code. Nobody’s quite sure where to put a new feature, so every pull request feels like a small argument. Sound familiar?
That’s exactly why Next.js App Router project structure best practices matter. The App Router gives you a lot of flexibility, but flexibility without a plan usually creates mess. The goal isn’t to make the folder tree look pretty. The goal is to help product teams ship faster, reduce decision fatigue, and keep the app easy to grow.
At Lunar Labs, we care a lot about this because structure affects everything downstream: onboarding, speed, quality, and even how confidently a team can add new features. A good structure doesn’t just support development. It supports the product.
Why project structure matters more in App Router
The App Router changed how Next.js apps are organized. That’s a good thing, but it also means old habits don’t always fit. If you’re still organizing everything the way you did in the Pages Router, you’ll likely hit friction fast.
Here’s the core issue: App Router encourages route-centric thinking, but product teams need feature-centric thinking too. Those aren’t the same thing.
A route might be /dashboard/settings/billing. A feature might be “billing preferences,” and that could include UI, server actions, schema validation, helpers, and tests. If those pieces are split across five unrelated folders, your team will waste time just finding the right file.
My opinion? The best Next.js App Router project structure best practices are the ones that make intent obvious. A developer should be able to open the repo and say, “I know where this lives.”
Start with a structure that mirrors the product
For most product teams, a clean structure looks something like this:
src/
app/
features/
components/
lib/
hooks/
styles/
types/
assets/
That’s not the only valid setup, but it’s a solid default. Why? Because it separates concerns without over-engineering the repo.
What each folder should do
-
app/
Route files, layouts, loading states, error boundaries, and route-specific logic. -
features/
Self-contained product areas like auth, billing, onboarding, search, or notifications. -
components/
Shared UI pieces that don’t belong to a single feature. -
lib/
Utilities, API clients, server helpers, and business logic that multiple parts of the app use. -
hooks/
Shared React hooks. -
types/
Global TypeScript types, though I’d keep most types near the feature that uses them. -
styles/
Global styles, design tokens, and theme files.
This kind of setup works well because it keeps route concerns in app/, while the actual product logic lives somewhere more intentional. That separation becomes very useful once the app grows.
Keep route files thin
One of the biggest mistakes teams make is stuffing too much into page.tsx or layout.tsx. It feels efficient at first. Then the file grows arms and legs and nobody wants to touch it.
A better approach is to keep route files thin and delegate real work elsewhere.
Good route file responsibilities
A route file should mostly handle:
- fetching route-level data
- composing feature components
- defining metadata
- setting up loading and error states
It shouldn’t contain:
- large UI blocks
- reusable business logic
- form validation rules
- complex data transformation
- tightly coupled helper functions
If a page starts reading like a small application in itself, that’s usually a sign the structure needs a reset.
For example, a billing page might look like this:
app/
dashboard/
billing/
page.tsx
loading.tsx
error.tsx
Then the actual billing experience can live in:
features/
billing/
components/
actions/
schemas/
utils/
That separation keeps the route readable and the feature reusable. I like this because it makes code review easier too. Reviewers can focus on one layer at a time instead of untangling a giant page file.
Organize by feature, not just by file type
This is where many teams go wrong. They create a perfectly tidy “components,” “hooks,” “utils,” and “services” structure, then scatter every feature across all of them. It looks organized from 10,000 feet, but in practice it’s a scavenger hunt.
A better pattern is to group feature-specific code together.
Example: auth feature
features/
auth/
components/
login-form.tsx
social-login-buttons.tsx
actions/
login.ts
logout.ts
schemas/
auth-schema.ts
utils/
redirect-after-login.ts
Example: onboarding feature
features/
onboarding/
components/
onboarding-step.tsx
progress-indicator.tsx
actions/
complete-step.ts
hooks/
use-onboarding-progress.ts
This approach gives each feature a home. That matters when teams scale. New engineers don’t need to guess where onboarding logic lives. They go to features/onboarding, and the picture becomes clear.
Honestly, this is one of the most practical Next.js App Router project structure best practices for product teams. It reduces mental overhead, which is a quiet but huge productivity win.
Use app/ for routing, not your whole app
The App Router folder has a strong identity, and that identity should stay focused. Use it for routing concerns, not as a dumping ground for every part of the application.
A good app/ structure
app/
(marketing)/
page.tsx
pricing/
page.tsx
(dashboard)/
layout.tsx
dashboard/
page.tsx
settings/
page.tsx
Route groups like (marketing) and (dashboard) are extremely helpful. They let you separate parts of the app without changing the URL structure. That’s handy for teams building both a public site and a logged-in product.
Put these in app/ when they belong to a route
page.tsxlayout.tsxloading.tsxerror.tsxnot-found.tsx- route-specific metadata
- route-specific server logic
Avoid putting these in app/
- shared UI libraries
- business rules used across features
- unrelated utility functions
- global state containers
- generic form components
My take: app/ should feel like the front door, not the whole house.
Keep shared components truly shared
Shared components are valuable, but teams often call something “shared” before it’s actually shared. That creates clutter fast.
A component belongs in components/ only if it’s used in multiple features or routes. If it’s only for billing, keep it in features/billing. If it’s only for onboarding, same deal.
Strong candidates for shared components
- buttons
- modals
- dropdowns
- badges
- empty states
- form inputs
- table wrappers
- navigation elements
Weak candidates for shared components
- billing plan cards
- onboarding step cards
- user invite forms
- dashboard analytics tiles
Those are usually feature-specific, even if they look reusable at first.
I’d also suggest keeping shared components small and composable. A giant “universal card” that tries to support every possible use case usually becomes impossible to maintain. Simpler wins here.
Separate server logic from UI logic
Next.js App Router makes server-side code a first-class citizen, which is great. But that also means teams can blur the line between presentation and execution if they’re not careful.
A healthy structure keeps:
- server actions in one place
- data access in another
- UI in a separate layer
- validation close to the feature
That separation helps avoid the classic problem where a React component is doing everything: fetching, validating, mutating, and rendering. That might work for a prototype. It’s painful in a production product.
Example layout for a feature
features/
project-invite/
components/
invite-form.tsx
actions/
send-invite.ts
schemas/
invite-schema.ts
data/
invite-queries.ts
That setup makes it obvious where side effects live and where UI lives. It also helps when you need to test or refactor a single part of the feature.
Be disciplined with lib/
lib/ is useful, but it can turn into a miscellaneous drawer if nobody guards it.
A good rule: put code in lib/ only if it’s broadly reusable and doesn’t clearly belong to one feature.
Good examples for lib/
- database client setup
- authentication helpers
- date formatting utilities
- API client wrappers
- environment validation
- shared server functions
Bad examples for lib/
- billing-specific calculations
- onboarding-only helpers
- feature-specific fetch functions
- UI helper functions that belong next to the component
If a helper only supports one feature, it should usually live with that feature. That’s one of those small decisions that pays off later, especially on teams with multiple developers moving quickly.
Co-locate tests and feature files
Testing becomes easier when the code under test lives in a predictable place. Co-location helps.
Instead of a giant global tests/ folder with hundreds of files, consider placing tests near the code they cover.
features/
billing/
components/
billing-summary.tsx
billing-summary.test.tsx
This works especially well for component tests and utility tests. For end-to-end tests, a separate e2e/ folder still makes sense.
Why do I prefer this? Because developers are more likely to maintain tests when they sit right next to the feature. If the test is hidden in a distant folder, it tends to get ignored.
Use aliases to reduce import chaos
Import paths can become ugly fast in a deeply nested App Router project. Nobody wants to read this all day:
import { Button } from "../../../../components/button"
Use aliases instead:
import { Button } from "@/components/button"
That single change improves readability and makes refactoring safer. It also reduces the urge to create weird folder shortcuts just to make imports less annoying.
A clean tsconfig.json alias setup is one of those boring improvements that quietly makes the whole codebase feel more professional.
Don’t over-nest folders
Some teams try to make the project structure “future proof” by adding layer after layer of folders. That usually backfires.
You don’t need this:
features/
billing/
components/
forms/
fields/
inputs/
That’s not structure. That’s a maze.
Instead, keep folders shallow until there’s a real reason to split them. If a folder only contains one file, ask whether it deserves to exist at all. I’m a fan of clarity over ceremony.
A practical structure example for a SaaS product
Here’s a layout I’d feel comfortable using for a growing SaaS app:
src/
app/
(marketing)/
page.tsx
pricing/
page.tsx
(dashboard)/
layout.tsx
dashboard/
page.tsx
settings/
page.tsx
features/
auth/
components/
actions/
schemas/
billing/
components/
actions/
schemas/
onboarding/
components/
actions/
hooks/
workspace/
components/
data/
utils/
components/
ui/
button.tsx
input.tsx
modal.tsx
navigation/
top-nav.tsx
side-nav.tsx
lib/
auth.ts
db.ts
env.ts
formatters.ts
hooks/
use-media-query.ts
types/
index.ts
This setup gives the team room to grow without throwing everything into one pile. It also supports common SaaS needs like auth, billing, onboarding, and workspace management without making the app impossible to navigate.
If you’re building a product with a real roadmap, this kind of foundation matters. It’s the difference between adding features smoothly and constantly fighting the repo.
If your team is planning a SaaS build or a redesign, Lunar Labs offers product strategy and discovery and web development services that can help you shape the right foundation from day one.
Team conventions matter as much as folder structure
A folder tree alone won’t save you. Teams need conventions. Otherwise, even a good structure decays over time.
Set rules for:
- where feature code goes
- what counts as shared UI
- how server actions are named
- whether components use PascalCase or kebab-case
- how to handle route-specific data fetching
- where validation schemas live
Write these rules down. Short, clear documentation beats tribal knowledge every time.
My opinion: one hour spent defining conventions can save weeks of confusion later. That’s not an exaggeration. It’s just what happens when multiple developers touch the same codebase.
Common mistakes to avoid
A lot of teams make the same structural mistakes. Here are the big ones.
1. Putting everything in components/
This creates a dumping ground. Feature context disappears, and the codebase becomes harder to understand.
2. Letting route files become giant feature files
Route files should compose, not dominate.
3. Overusing lib/
If everything is “shared,” nothing is actually organized.
4. Splitting by file type too early
A pure components/hooks/utils/services setup often works against real product development.
5. Ignoring naming conventions
If names are vague, folders won’t save you. “Stuff.tsx” is not a helpful file name.
How Lunar Labs approaches structure
At Lunar Labs, we look at structure as part of product design, not just engineering housekeeping. That matters because the best codebase is one your team can understand, extend, and trust.
For startups and product teams, we usually start by answering a few questions:
- What are the core product areas?
- Which parts of the app will change the most?
- What should be reusable, and what should stay local?
- How many engineers will touch this in the next 12 months?
- Where do server actions and data models belong?
That conversation often shapes the architecture more than any framework decision. And honestly, that’s how it should be. The code should serve the product, not the other way around.
If your team is exploring a new product build, a design partner for SaaS products can help you connect UX, structure, and implementation before problems harden into the codebase.
Final checklist for a healthy App Router structure
Before you ship, ask yourself:
- Does
app/only handle routing concerns? - Are feature files grouped by product area?
- Are shared components truly shared?
- Is server logic separated from UI?
- Are imports clean and readable?
- Can a new developer find the billing or auth flow in under a minute?
- Do folder names match the product language your team actually uses?
If the answer to most of those is yes, you’re in good shape.
Build the structure your product deserves
A good Next.js app doesn’t happen by accident. It takes a structure that matches how the product works, how the team thinks, and how the app will grow.
That’s the real point behind Next.js App Router project structure best practices. Not dogma. Not rigid rules. Just a practical way to keep your codebase fast, understandable, and ready for the next stage.
If you’re planning a Next.js product and want a team that can help shape the architecture, design the experience, and build the app with scale in mind, Lunar Labs can help. Start with strategy and discovery or explore our web development services to see how we work.
A cleaner structure today means fewer headaches tomorrow. And if you’ve ever spent an afternoon untangling a messy repo, you already know that’s worth it.