Next.js Project Structure: Folder Organization Best Practices
Learn how to structure Next.js applications for scalability.
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
- ✓Node.js 18+, npm/yarn/pnpm, basic knowledge of React and Next.js, TypeScript familiarity recommended
- Use
src/directory for application code,app/for routes,components/for shared UI - Private folders prefixed with
_prevent accidental route exposure - Route groups
(marketing)organize sections without affecting URLs - Colocate tests, styles, and data fetching near the components that use them
- Keep
lib/for business logic,actions/for server actions,db/for database access - Root layout must contain
andtags
Think of a Next.js project structure like a well-organized workshop. Every tool has a designated drawer (components), raw materials have a shelf (lib), and the workbench (app directory) only holds what you're actively building. A messy workshop slows you down — a clean structure makes you faster.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Next.js project structure decisions made early affect every build, deploy, and developer onboarding that follows. The App Router's file-system routing gives you powerful organizational primitives — private folders, route groups, layouts, and parallel routes — but without a consistent structure, these tools create confusion instead of clarity.
This guide covers proven patterns for organizing Next.js applications, from solo projects to enterprise monorepos. You'll learn where to put files, how to use App Router conventions effectively, and which patterns scale.
The Minimal Starter: What Next.js Gives You
When you run npx create-next-app@latest, you get a bare-bones structure: pages/ (or app/ if using the App Router), public/, styles/, and config files like next.config.js. This is intentionally minimal to avoid imposing opinions. However, for any real project, this flat layout quickly becomes unmanageable. The pages/ directory (or app/ directory) is the heart of routing, but dumping all components, utilities, and API routes there is a recipe for disaster. The default structure is a starting point, not a production-ready architecture. You must extend it with a logical separation of concerns.
app/ with page.tsx and layout.tsx instead of pages/. Adjust folder names accordingly.pages/ for six months. When we hit 50+ pages, imports became a tangled mess, and renaming a component broke three routes. We spent a week refactoring.The Core Principle: Separation by Feature, Not by Type
The most common mistake beginners make is grouping files by type: components/, utils/, hooks/, types/. This works for tiny apps but fails at scale because related code is scattered across folders. Instead, organize by feature or domain. For example, a UserProfile feature would have its own folder containing the component, its styles, its tests, its hooks, and its API calls. This makes it easy to locate all code related to a feature, simplifies deletion or replacement, and reduces merge conflicts because teams work on different features. The top-level folders become features/, shared/, lib/, and config/. This is often called the 'feature-first' or 'domain-driven' structure.
index.ts files to re-export public API of a feature. This hides internal structure and allows refactoring without changing imports elsewhere.The `pages/` Directory: Keep It Thin
The pages/ directory (or app/ in App Router) should only contain route files that are thin wrappers. Each page file should import a feature component and pass props. Never put business logic, data fetching, or complex UI directly in a page file. This keeps routing concerns separate from feature logic. For example, pages/dashboard.tsx should just render <DashboardPage /> from features/dashboard/. This also makes it easy to test pages in isolation and swap out features without touching routing. If you use Next.js API routes, keep them in pages/api/ but again, keep them thin — delegate to service functions in features/ or lib/.
The `shared/` Directory: Reusable UI and Utilities
The shared/ folder contains code that is used across multiple features. This includes UI primitives (Button, Input, Modal), layout components (Header, Footer), utility functions (date formatting, validation), and custom hooks that are not feature-specific. Each shared UI component should be a self-contained folder with its component file, styles, tests, and an index.ts barrel export. This makes components easy to find, test, and reuse. Avoid putting feature-specific logic in shared — that's what feature folders are for. Keep shared small and generic; if a component starts accumulating feature-specific props, it's time to move it into a feature.
shared/ui/ as your internal component library. Use Storybook to document components and enforce consistency.Button that grew 20 props for different features. We split it into Button (generic) and feature-specific wrappers. Code review time dropped.The `lib/` Directory: Pure Logic and Configuration
The lib/ folder holds pure functions, configuration, and third-party integrations that are not tied to any feature. Examples: API client setup (Axios instance), database connection, authentication helpers, environment variable parsing, and constants. This folder should have minimal dependencies on React or Next.js — it's the 'backend' of your frontend. Keep it flat or group by concern (e.g., lib/api/, lib/db/, lib/constants/). Avoid putting React hooks or components here; those belong in shared/ or features/. The lib/ folder is also where you put utility functions that are used across features, like formatCurrency or validateEmail.
lib/constants/env.ts to provide type safety and default values. Never access process.env directly in components.lib/api/client.ts made it easy to add retry logic and error tracking.The `public/` Directory: Static Assets Only
The public/ folder is for static assets that are served directly by the web server: images, fonts, robots.txt, favicon, and other files that don't need processing. Next.js automatically serves files from public/ at the root URL. For example, public/logo.png is accessible at /logo.png. Do not put source code, compiled files, or anything that needs to be imported here. For images that are used in components, consider using the next/image component with a remote URL or placing them in public/images/. Organize by type: public/images/, public/fonts/, public/icons/. Avoid deep nesting.
public/ are not processed by Webpack. If you need to import an image in a component, use import from a relative path or use next/image with a remote URL.public/ and imported it via fetch. It worked locally but broke in production because the file wasn't included in the build. Use lib/ for config.The `styles/` Directory: Global and Module CSS
Next.js supports global CSS and CSS Modules. The styles/ folder typically contains globals.css for global styles (reset, fonts, CSS variables) and module CSS files for component-specific styles. However, with the rise of Tailwind CSS and CSS-in-JS, many projects no longer use a dedicated styles/ folder. If you use Tailwind, you only need globals.css for Tailwind directives. If you use CSS Modules, co-locate the .module.css file with the component in its feature folder, not in a global styles/ folder. This follows the co-location principle: keep styles close to the component they style.
.module.css file next to the component. This makes it easy to delete a component without orphaned styles.styles/ folder with 30 CSS files. When we removed a feature, we often forgot to delete the corresponding CSS file, leading to dead code. Co-location solved this.styles/, component styles co-located with components.The `types/` Directory: Shared TypeScript Types
If your project uses TypeScript (and it should), you need a place for shared types and interfaces. The types/ folder at the root is for types that are used across multiple features, such as API response types, user types, or global enums. Feature-specific types should live inside the feature folder (e.g., features/auth/types/auth.ts). This keeps types close to where they are used and reduces import complexity. Export all shared types from an index.ts barrel file. Avoid putting types in lib/ or shared/ unless they are truly generic.
Nullable<T>), create a types/global.d.ts file. But prefer explicit imports over global types.User types in three features. Merging into types/user.ts eliminated inconsistencies that caused runtime errors.types/, feature types co-located.The `tests/` Directory: Testing Strategy
Testing is non-negotiable. The tests/ folder at the root is for integration and end-to-end tests (e.g., Playwright, Cypress). Unit tests should be co-located with the component or function they test, using the .test.tsx or .spec.ts suffix. This co-location makes it obvious when a file lacks tests and ensures tests are deleted when the source is deleted. For shared utilities, place tests in a __tests__/ folder inside lib/ or alongside the utility file. Configure Jest or Vitest to pick up files with .test. pattern. Avoid a separate __tests__/ folder at the root for unit tests — it disconnects tests from code.
__tests__/ folder with 200 test files. When we deleted a feature, we often missed deleting its tests, leading to stale tests that passed but tested nothing.tests/ root.The `config/` Directory: Environment and App Config
The config/ folder holds configuration files that are not environment variables but are still project-wide settings. Examples: config/site.ts for site metadata (title, description, URL), config/navigation.ts for navigation links, config/seo.ts for default SEO settings. This separates configuration from logic and makes it easy to find and change settings without digging through code. Environment variables should still go in .env.local and be accessed via lib/constants/env.ts. The config/ folder is for static, non-sensitive configuration that is committed to the repo.
The `hooks/` Directory: Custom React Hooks
Custom hooks that are shared across multiple features belong in a top-level hooks/ folder. Feature-specific hooks should stay in the feature folder. The hooks/ folder is for hooks like useDebounce, useLocalStorage, useMediaQuery, or useIntersectionObserver. These are generic enough to be used anywhere. Keep each hook in its own file, named after the hook (e.g., useDebounce.ts). Export from an index.ts barrel. Avoid putting hooks that depend on a specific feature's types or API here — that's a sign they should be in the feature folder.
use to follow React conventions and enable linting rules.useAuth hook in hooks/ that imported feature-specific types. When we refactored auth, we broke three other features. Now feature hooks stay in features.hooks/, feature hooks co-located.Putting It All Together: A Production-Ready Structure
Here's the final recommended structure for a Next.js project using the Pages Router. Adjust for App Router by replacing pages/ with app/ and using layout.tsx and page.tsx. The key is consistency: every team member should understand where to put new code. Use a CONTRIBUTING.md to document the structure. Enforce it with ESLint rules (e.g., no imports from features/ into other features except through shared). This structure scales to hundreds of pages and dozens of developers. It's not perfect for every project, but it's a proven starting point that avoids the most common pitfalls.
features/, shared/, and lib/. Add folders as you grow. The goal is clarity, not complexity.| File | Command / Code | Purpose |
|---|---|---|
| terminal | npx create-next-app@latest my-app --typescript --tailwind --eslint | The Minimal Starter |
| terminal | my-app/ | The Core Principle |
| pages | interface DashboardProps { | The `pages/` Directory |
| shared | interface ButtonProps extends React.ButtonHTMLAttributes | The `shared/` Directory |
| lib | const apiClient = axios.create({ | The `lib/` Directory |
| terminal | tree public/ -L 2 | The `public/` Directory |
| styles | @tailwind base; | The `styles/` Directory |
| types | export interface User { | The `types/` Directory |
| features | describe('LoginForm', () => { | The `tests/` Directory |
| config | export const siteConfig = { | The `config/` Directory |
| hooks | export function useDebounce | The `hooks/` Directory |
Key takeaways
pages/ (or app/) as thin wrappers that delegate to feature components. All logic lives in feature folders.shared/ and lib/shared/ui/, pure logic and configuration go in lib/. Feature-specific code stays in features.Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
That's Next.js. Mark it forged?
4 min read · try the examples if you haven't