# Scalable Next.js Project Structure with App Router

> A battle-tested folder structure for Next.js 15 App Router projects that scales from side project to production SaaS.

URL: https://www.matthewswong.com/en/blog/nextjs-project-structure-scalable/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2026-10-17
- Category: Web Dev

Every Next.js project starts simple — a handful of pages, some components, maybe a lib folder. Then three months in, you're hunting for a utility function buried somewhere in a component file, and your app directory looks like a digital hoarder's attic. I've rebuilt this structure twice on real projects, and the pattern I'm sharing here is the one I actually use for web apps.

## Why Structure Matters More in App Router

The App Router introduced a fundamentally different mental model. Files inside app/ can be Server Components, Client Components, layouts, loading states, error boundaries, or API routes — all colocated. Without a deliberate folder strategy, this becomes chaotic fast. The structure I use separates concerns cleanly: routing lives in app/[locale]/(pages)/, shared components in app/components/, utilities in app/lib/, and so on.

### The Route Group Pattern

Route groups (folders wrapped in parentheses) let you organize routes without affecting the URL. I use (pages) to group all user-facing pages under the locale prefix without adding 'pages' to the URL. This means /en/about resolves from app/[locale]/(pages)/about/page.tsx. You can also use route groups to apply different layouts — for example, a (dashboard) group with a sidebar layout separate from (marketing) pages with a plain navbar.

### Colocation vs Separation

A common debate: should feature components live next to their routes, or in a centralized components folder? My rule: if a component is only used by one route, colocate it. If it's shared across two or more routes, move it to app/components/. This prevents the 'where does this live?' problem. For a blog, BlogCard and BlogFilters only belong to the blog listing — so they live in app/[locale]/(pages)/blog/. But Navbar and Footer are global, so they're in app/components/layout/.

> Use barrel exports (index.ts files) in your component folders. Instead of importing from deep paths like @/components/ui/Button, you export everything from @/components/ui/index.ts and import '{ Button, Input, Modal }' from '@/components/ui'. This makes refactoring internal file names painless.

## TypeScript Path Aliases and Barrel Exports

Path aliases are non-negotiable on any project with more than 3 developers or that will live longer than a month. Relative imports like '../../../lib/db' are a maintenance nightmare. Configure tsconfig.json paths once, use @/lib/db everywhere. The singleton Prisma client pattern is also critical — instantiating PrismaClient on every hot module reload in development will exhaust your database connection pool within minutes.

### Environment Variables and Config

Keep all environment variable parsing in one place: app/lib/env.ts. Use the t3-env or zod-based pattern to validate env vars at startup, not at runtime when a user triggers a code path. This catches missing STRIPE_SECRET_KEY or DATABASE_URL before you deploy, not in production at 2am. Export typed constants from this file so every consumer gets proper autocomplete and no raw process.env access scattered through the codebase.

## Feature-Based vs Layer-Based Organization

Layer-based (controllers/, services/, repositories/) works well for backend NestJS apps. For a Next.js frontend, I prefer feature-based grouping inside components/. A 'products' feature folder contains ProductCard.tsx, ProductFilters.tsx, ProductTable.tsx, and useProducts.ts. This makes it easy to delete or move an entire feature without hunting through multiple directories.

> **Avoid deeply nesting route groups**

> Nesting route groups more than two levels deep creates Next.js layout inheritance chains that are hard to reason about. If you find yourself at app/[locale]/(pages)/(dashboard)/(settings)/profile/page.tsx, consider flattening. The route group is for layout sharing, not for expressing hierarchy that isn't in the URL.

## Handling i18n in the Structure

The [locale] dynamic segment at the root gives you internationalization without a separate i18n directory sprawl. All translations live in messages/en.json and messages/id.json at the project root. The next-intl library handles the rest — useTranslations() in Client Components, getTranslations() in Server Components. Keep translation key namespaces consistent with your feature structure: blog.posts.*, dashboard.sidebar.*, common.* for shared labels.

## The Final Checklist Before You Scale

Before a project grows beyond MVP, run through this: (1) Are all shared utilities in lib/, not scattered in components? (2) Do all Server Components stay server-side — no useState or useEffect imports? (3) Is Prisma instantiated as a singleton? (4) Do API routes use proper error handling with consistent response shapes? (5) Are environment variables validated at startup? (6) Is there a single tsconfig paths config that the whole team uses? Structure is cheapest to fix before you have 50 files depending on the wrong pattern.

## Sources & Further Reading

- Vercel / Next.js — Project Structure & File Conventions — https://nextjs.org/docs/app/getting-started/project-structure
- Vercel — How We Optimized Package Imports in Next.js — https://vercel.com/blog/how-we-optimized-package-imports-in-next-js
- TypeScript — tsconfig paths reference — https://www.typescriptlang.org/tsconfig#paths

## Source links

- https://nextjs.org/docs/app/getting-started/project-structure
- https://vercel.com/blog/how-we-optimized-package-imports-in-next-js
- https://www.typescriptlang.org/tsconfig#paths

## FAQ

### Why use route groups like (pages) in the App Router, and do they affect the URL?

Route groups — folders wrapped in parentheses — let you organize routes and share layouts without adding any segment to the URL. For example, placing pages under app/[locale]/(pages)/ means /en/about resolves from app/[locale]/(pages)/about/page.tsx, keeping the URL clean. You can also use separate route groups like (dashboard) and (marketing) to apply different layouts to different sections of the app.

### When should a component be colocated next to its route versus moved to the shared components folder?

The rule is straightforward: if a component is only used by a single route, keep it colocated with that route. As soon as two or more routes share a component, move it to app/components/. For example, BlogCard and BlogFilters belong inside the blog route folder, while Navbar and Footer — used across every page — live in app/components/layout/.

### Why are TypeScript path aliases and barrel exports considered non-negotiable for scalable projects?

Relative imports like '../../../lib/db' become a maintenance nightmare as the project grows, especially across a team of more than three developers. Configuring tsconfig.json paths once (e.g. @/lib/db) and using barrel exports (index.ts) means internal file reorganisation never breaks import statements elsewhere. Barrel exports also enable clean imports like '{ Button, Input, Modal }' from '@/components/ui' instead of deep per-file paths.

### Why must the Prisma client be instantiated as a singleton, and what happens if it is not?

In Next.js development mode, hot module reloads can trigger multiple PrismaClient instantiations. Each instance opens its own database connection pool, so without a singleton pattern the pool is exhausted within minutes. The singleton ensures only one PrismaClient instance exists per Node.js process, keeping connection usage predictable.

### How does storing environment variable parsing in a single file improve reliability?

Centralising all env var parsing in app/lib/env.ts — validated with t3-env or a zod-based pattern — catches missing variables like DATABASE_URL or STRIPE_SECRET_KEY at application startup rather than when a user triggers a code path at runtime. Exporting typed constants from that file also eliminates raw process.env access scattered through the codebase, providing proper autocomplete and preventing silent misconfiguration that surfaces in production.

---

- HTML version: https://www.matthewswong.com/en/blog/nextjs-project-structure-scalable/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/nextjs-project-structure-scalable/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
