Home JavaScript Next.js Project Structure: Folder Organization Best Practices
Beginner 4 min · July 12, 2026

Next.js Project Structure: Folder Organization Best Practices

Learn how to structure Next.js applications for scalability.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 20 min
  • Node.js 18+, npm/yarn/pnpm, basic knowledge of React and Next.js, TypeScript familiarity recommended
Quick Answer
  • 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 and tags
✦ Definition~90s read
What is Next.js Project Structure?

Next.js project structure is the deliberate organization of files and folders in a Next.js application to maximize maintainability, scalability, and developer efficiency. It matters because a chaotic structure leads to merge conflicts, slow onboarding, and hidden bugs. Use it from day one of any Next.js project, whether a small prototype or a large enterprise app.

Think of a Next.js project structure like a well-organized workshop.
Plain-English First

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.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

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.

terminalBASH
1
2
3
npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app
tree -L 2 -I node_modules
Output
my-app/
├── pages/
│ ├── api/
│ │ └── hello.ts
│ ├── _app.tsx
│ └── index.tsx
├── public/
│ ├── favicon.ico
│ └── vercel.svg
├── styles/
│ ├── Home.module.css
│ └── globals.css
├── next.config.js
├── package.json
├── tsconfig.json
└── ...
App Router vs Pages Router
This article focuses on the Pages Router for stability, but the same principles apply to the App Router. The App Router uses app/ with page.tsx and layout.tsx instead of pages/. Adjust folder names accordingly.
Production Insight
In a past project, we kept everything in 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.
Key Takeaway
Never use the default structure as-is for production; it's a scaffold, not a blueprint.
nextjs-project-structure-best-practices THECODEFORGE.IO Next.js Folder Architecture: Layered Separation Feature-based organization for maintainable code Presentation Layer pages/ | shared/ Logic Layer lib/ | utils/ Data Layer types/ | config/ Asset Layer public/ | styles/ THECODEFORGE.IO
thecodeforge.io
Nextjs Project Structure Best Practices

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.

terminalBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
my-app/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── SignupForm.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── api/
│   │   │   └── authApi.ts
│   │   ├── types/
│   │   │   └── auth.ts
│   │   └── index.ts
│   └── dashboard/
│       ├── components/
│       │   ├── DashboardLayout.tsx
│       │   └── StatsCard.tsx
│       ├── hooks/
│       │   └── useDashboardData.ts
│       └── api/
│           └── dashboardApi.ts
├── shared/
│   ├── ui/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.test.tsx
│   │   │   └── index.ts
│   │   └── Modal/
│   │       └── ...
│   └── lib/
│       └── formatDate.ts
├── pages/
│   ├── index.tsx
│   ├── login.tsx
│   └── dashboard.tsx
└── ...
Barrel Files
Use index.ts files to re-export public API of a feature. This hides internal structure and allows refactoring without changing imports elsewhere.
Production Insight
At a startup, we switched from type-based to feature-based folders. Onboarding time dropped from 2 weeks to 3 days because new devs could find code intuitively.
Key Takeaway
Group by feature, not by file type — it scales with your team and codebase.

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/.

pages/dashboard.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { GetServerSideProps } from 'next';
import { DashboardPage } from '@/features/dashboard';
import { getDashboardData } from '@/features/dashboard/api';

interface DashboardProps {
  data: DashboardData;
}

export default function Dashboard({ data }: DashboardProps) {
  return <DashboardPage data={data} />;
}

export const getServerSideProps: GetServerSideProps<DashboardProps> = async () => {
  const data = await getDashboardData();
  return { props: { data } };
};
Try it live
Avoid Mixing Concerns
Resist the urge to put inline styles, state, or effects in page files. If a page needs local state, create a wrapper component in the feature folder.
Production Insight
We once had a page with 400 lines of inline data fetching and UI. When the API changed, we had to update three pages instead of one feature. Thin pages saved us.
Key Takeaway
Pages are thin shells; all logic lives in feature folders.
Feature-Based vs. Type-Based Folder Organization Comparing maintainability and scalability Feature-Based Type-Based Folder Structure Group by feature (e.g., auth/, blog/) Group by type (e.g., components/, utils/ Scalability High: features are self-contained Medium: cross-type dependencies grow Reusability Moderate: shared code in shared/ High: all components in one place Onboarding Easy: new devs see feature context Harder: need to navigate multiple type f Refactoring Easy: change one feature folder Hard: changes ripple across type folders THECODEFORGE.IO
thecodeforge.io
Nextjs Project Structure Best Practices

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/Button/Button.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
}

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'md',
  children,
  ...props
}) => {
  const baseClasses = 'rounded font-semibold focus:outline-none focus:ring-2';
  const variantClasses = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
    danger: 'bg-red-600 text-white hover:bg-red-700',
  };
  const sizeClasses = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg',
  };

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
      {...props}
    >
      {children}
    </button>
  );
};
Try it live
Component Library Pattern
Treat shared/ui/ as your internal component library. Use Storybook to document components and enforce consistency.
Production Insight
We had a shared Button that grew 20 props for different features. We split it into Button (generic) and feature-specific wrappers. Code review time dropped.
Key Takeaway
Shared code is generic and reusable; feature code is specific and encapsulated.

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/api/client.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('authToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Redirect to login
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default apiClient;
Try it live
Environment Variables
Store all env vars in lib/constants/env.ts to provide type safety and default values. Never access process.env directly in components.
Production Insight
We once had API client setup scattered across 10 files. Consolidating into lib/api/client.ts made it easy to add retry logic and error tracking.
Key Takeaway
Lib is for pure logic and configuration — keep it framework-agnostic.

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.

terminalBASH
1
tree public/ -L 2
Output
public/
├── favicon.ico
├── robots.txt
├── images/
│ ├── logo.svg
│ └── hero.jpg
├── fonts/
│ └── Inter-Variable.woff2
└── icons/
├── facebook.svg
└── twitter.svg
Don't Import from Public
Files in 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.
Production Insight
A teammate once put a JSON config file in 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.
Key Takeaway
Public is for static files only — never put source code there.

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.

styles/globals.cssCSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  body {
    @apply bg-gray-50 text-gray-900 antialiased;
  }
}

@layer components {
  .btn-primary {
    @apply bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700;
  }
}
Try it live
Co-locate Styles
If using CSS Modules, put the .module.css file next to the component. This makes it easy to delete a component without orphaned styles.
Production Insight
We had a 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.
Key Takeaway
Global styles in 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.

types/user.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user';
  createdAt: string;
}

export interface AuthResponse {
  user: User;
  token: string;
}

export type LoginCredentials = {
  email: string;
  password: string;
};
Try it live
Global Types
For truly global types (e.g., Nullable<T>), create a types/global.d.ts file. But prefer explicit imports over global types.
Production Insight
We once had duplicate User types in three features. Merging into types/user.ts eliminated inconsistencies that caused runtime errors.
Key Takeaway
Shared types in 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.

features/auth/components/LoginForm.test.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  it('renders email and password fields', () => {
    render(<LoginForm />);
    expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
  });

  it('calls onSubmit with credentials', () => {
    const handleSubmit = jest.fn();
    render(<LoginForm onSubmit={handleSubmit} />);
    fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'test@test.com' } });
    fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'password123' } });
    fireEvent.click(screen.getByRole('button', { name: /log in/i }));
    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@test.com',
      password: 'password123',
    });
  });
});
Try it live
Test Co-location
Place unit tests next to the source file. This makes it easy to see test coverage and ensures tests are removed when code is removed.
Production Insight
We had a separate __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.
Key Takeaway
Unit tests co-located, integration tests in 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.

config/site.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
export const siteConfig = {
  name: 'TheCodeForge',
  description: 'Production-focused technical blog',
  url: 'https://thecodeforge.io',
  ogImage: 'https://thecodeforge.io/og.jpg',
  links: {
    twitter: 'https://twitter.com/thecodeforge',
    github: 'https://github.com/thecodeforge',
  },
};

export type SiteConfig = typeof siteConfig;
Try it live
Don't Hardcode
Never hardcode URLs, titles, or other config in components. Use the config folder and import where needed.
Production Insight
We once had the site title hardcoded in 10 places. When we rebranded, we had to do a find-and-replace. Now it's in one file.
Key Takeaway
Config folder centralizes non-sensitive, static settings.

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.

hooks/useDebounce.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { useState, useEffect } from 'react';

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}
Try it live
Hook Naming
Always prefix custom hooks with use to follow React conventions and enable linting rules.
Production Insight
We had a useAuth hook in hooks/ that imported feature-specific types. When we refactored auth, we broke three other features. Now feature hooks stay in features.
Key Takeaway
Shared hooks in 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.

terminalBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
my-app/
├── config/
│   ├── site.ts
│   └── navigation.ts
├── features/
│   ├── auth/
│   │   ├── api/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── types/
│   │   └── index.ts
│   └── dashboard/
│       └── ...
├── hooks/
│   ├── useDebounce.ts
│   └── useLocalStorage.ts
├── lib/
│   ├── api/
│   │   └── client.ts
│   ├── constants/
│   │   └── env.ts
│   └── utils/
│       └── formatDate.ts
├── pages/
│   ├── api/
│   │   └── auth.ts
│   ├── _app.tsx
│   ├── index.tsx
│   ├── login.tsx
│   └── dashboard.tsx
├── public/
│   ├── images/
│   └── favicon.ico
├── shared/
│   ├── ui/
│   │   ├── Button/
│   │   └── Modal/
│   └── layout/
│       ├── Header.tsx
│       └── Footer.tsx
├── styles/
│   └── globals.css
├── tests/
│   └── e2e/
│       └── login.spec.ts
├── types/
│   ├── user.ts
│   └── index.ts
├── next.config.js
├── tsconfig.json
└── package.json
Don't Over-Engineer
Start simple. For a small project, you might only need features/, shared/, and lib/. Add folders as you grow. The goal is clarity, not complexity.
Production Insight
We once adopted a 15-folder structure from a blog post. It caused more confusion than it solved. Start with 4-5 folders and expand only when needed.
Key Takeaway
Consistency and simplicity are more important than following a rigid structure.
⚙ Quick Reference
11 commands from this guide
FileCommand / CodePurpose
terminalnpx create-next-app@latest my-app --typescript --tailwind --eslintThe Minimal Starter
terminalmy-app/The Core Principle
pagesdashboard.tsxinterface DashboardProps {The `pages/` Directory
shareduiButtonButton.tsxinterface ButtonProps extends React.ButtonHTMLAttributes {The `shared/` Directory
libapiclient.tsconst apiClient = axios.create({The `lib/` Directory
terminaltree public/ -L 2The `public/` Directory
stylesglobals.css@tailwind base;The `styles/` Directory
typesuser.tsexport interface User {The `types/` Directory
featuresauthcomponentsLoginForm.test.tsxdescribe('LoginForm', () => {The `tests/` Directory
configsite.tsexport const siteConfig = {The `config/` Directory
hooksuseDebounce.tsexport function useDebounce(value: T, delay: number): T {The `hooks/` Directory

Key takeaways

1
Feature-first organization
Group code by feature, not by file type. This makes it easy to find, delete, and refactor features independently.
2
Thin pages, fat features
Keep pages/ (or app/) as thin wrappers that delegate to feature components. All logic lives in feature folders.
3
Co-locate tests and styles
Place unit tests and CSS modules next to the component they test or style. This prevents orphaned files and makes deletion safe.
4
Shared code in shared/ and lib/
Reusable UI components go in shared/ui/, pure logic and configuration go in lib/. Feature-specific code stays in features.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Should I use the App Router or Pages Router for a new project?
02
How do I handle imports across features?
03
Where do I put API routes in this structure?
04
Can I use this structure with a monorepo?
05
How do I handle environment variables?
06
What if my project is very small? Should I still use this structure?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

4 min read · try the examples if you haven't

Previous
Pages Router to App Router Migration: Step-by-Step Guide
50 / 56 · Next.js
Next
Next.js with Drizzle ORM: Database Integration Guide