Structuring a Next.js project might seem straightforward at first, especially since Next.js comes with a lot of conventions out of the box. However, as your application grows, a well-thought-out folder and file organization becomes crucial for maintainability, scalability, and developer productivity. Over the years, I’ve worked on multiple Next.js projects ranging from small marketing sites to large-scale SaaS platforms, and I’ve learned that the right structure balances convention with flexibility.
Below, I’ll walk through practical advice on how to structure a Next.js project, including core concepts, real-world examples, common pitfalls, and performance considerations. I’ll also touch on security and interview tips, so you’re well-prepared to discuss this topic in a technical interview or apply it in production.
Next.js is built around the idea of convention over configuration. The framework expects certain folders like pages and public to exist and uses them to automatically generate routes and serve static assets. However, beyond these conventions, you have the freedom to organize your components, utilities, styles, and API routes in a way that suits your team and project size.
Here are some key folders and files you’ll encounter in a typical Next.js project:
pages/: Contains React components mapped to routes. Supports file-based routing.public/: Static assets like images, fonts, and robots.txt.components/: Reusable UI components.styles/: CSS, Sass, or other styling files.lib/ or utils/: Helper functions, API clients, and shared logic.api/ (inside pages/): Serverless API routes.hooks/: Custom React hooks.contexts/: React context providers for global state management.While Next.js doesn’t enforce these beyond pages and public, adopting a consistent structure helps keep your codebase clean and scalable.
Here’s a practical folder structure I’ve used in production projects that balances clarity and scalability:
/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.module.css
│ │ └── index.ts
│ ├── Header/
│ └── Footer/
├── contexts/
│ └── AuthContext.tsx
├── hooks/
│ └── useAuth.ts
├── lib/
│ ├── apiClient.ts
│ └── helpers.ts
├── pages/
│ ├── api/
│ │ └── auth.ts
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── index.tsx
│ ├── dashboard.tsx
│ └── profile/
│ └── [id].tsx
├── public/
│ ├── images/
│ └── favicon.ico
├── styles/
│ ├── globals.css
│ └── variables.css
├── types/
│ └── index.d.ts
├── .env.local
├── next.config.js
└── package.json
Let me explain some choices here:
import Button from 'components/Button').pages/api. Dynamic routes like [id].tsx are used for user profiles or similar.This approach works because it separates concerns clearly. Components are self-contained, making them easier to test and reuse. Hooks and contexts are isolated, which helps when you want to refactor or swap out state management solutions. The lib folder keeps your business logic or API clients decoupled from UI code, which is great for testing and scaling.
Also, by keeping the pages directory clean and focused on routing, you avoid mixing UI components with routing logic, which can get messy fast in larger projects.
pages: Sometimes developers put all components directly inside pages, which bloats the folder and makes it hard to find reusable components.Next.js optimizes performance through automatic code splitting and server-side rendering (SSR). Your project structure can impact how effectively these features work:
next/dynamic) for components like charts or maps that aren’t needed immediately.public: Store images, fonts, and other static files in public to serve them efficiently via CDN.While project structure doesn’t directly enforce security, organizing your code properly helps avoid common pitfalls:
pages/api or lib to avoid accidental exposure..env.local for secrets and never commit them to version control.| Aspect | Flat Structure | Feature-Based Structure | Domain-Driven Structure |
|---|---|---|---|
| Description | All components and pages in one or two folders. | Group files by feature or functionality. | Organize by business domain or bounded context. |
| Pros | Simple, easy to start. | Improves modularity, easier to scale. | Aligns with business logic, good for large apps. |
| Cons | Becomes messy quickly. | Can lead to duplicated utilities. | Requires upfront domain knowledge. |
| Use case | Small projects or prototypes. | Medium to large apps with clear features. | Enterprise-level apps with complex domains. |
Personally, I prefer a hybrid approach: feature-based organization for components and pages, with shared utilities and hooks centralized. This keeps things modular but avoids duplication.
In one SaaS project I worked on, we started with a flat structure but quickly ran into issues as the app grew. We refactored to a feature-based structure where each feature folder contained its pages, components, hooks, and tests. For example:
/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── pages/
│ │ └── api.ts
│ ├── dashboard/
│ └── profile/
This made onboarding new developers easier because they could focus on one feature at a time. It also improved our CI build times since we could run tests and linting per feature folder.
We kept global styles and shared components in their respective top-level folders. This balance helped us maintain clear boundaries while sharing common code efficiently.
Structuring a Next.js project isn’t just about following conventions; it’s about setting up your codebase for long-term success. A clean, modular structure improves maintainability, helps with performance optimizations, and makes scaling your app and team easier. Avoid dumping everything into pages or components, and think carefully about how your project will grow.
When preparing for interviews, focus on explaining your reasoning, trade-offs, and how your structure supports real-world challenges like testing, performance, and security. Sharing concrete examples from your experience will always stand out.