DevelopmentA Practical Guide to Next.js Project Structure
How I think about organizing components, features, data, and routes when building larger Next.js applications.
When a Next.js project grows past a certain point — somewhere around ten routes and fifteen components — the default structure starts to feel cramped. Files get lost. Imports become guessing games. Refactoring turns into archaeology.
After working on several production Next.js applications, I have settled on a structure that scales well without being overly prescriptive. It is not a framework — just a set of sensible defaults that can flex when the project demands it.
The Top-Level Structure
src/
├── app/ # Next.js App Router (routes only)
│ ├── (main)/ # Grouped layout routes
│ │ ├── about/
│ │ ├── blogs/
│ │ └── work/
│ ├── api/ # API route handlers
│ ├── globals.css
│ └── layout.tsx
├── components/ # Shared, reusable UI components
├── screens/ # Page-level screen components
├── data/ # Static JSON data files
├── hooks/ # Custom React hooks
├── lib/ # Utility functions & helpers
├── services/ # API client functions
└── providers/ # Context & global providersThe app/ Directory — Routes Only
With the App Router, I treat the app/ directory as a routing layer only. Page files stay thin — they import a screen component, pass props, and render. No business logic, no fetch logic, no JSX beyond what is strictly necessary for the layout.
import { BlogScreen } from '@/screens/blogs/BlogScreen';
import blogsData from '@/data/blogs.json';
export default function BlogPage() {
return <BlogScreen blogs={blogsData.blogs} />;
}Screens vs Components
This is the distinction that saves the most confusion. A component is reusable anywhere. A screen is a full-page layout that belongs to one specific route.
| Question | components/ | screens/ |
|---|---|---|
| Used in more than one page? | Yes | No |
| Contains page-level layout? | No | Yes |
| Has specific route context? | No | Yes |
| Generic enough to reuse? | Yes | No |
Data Files
For portfolio sites, personal projects, and small applications, static JSON in data/ is often the right call. No database overhead, no API layer, full type safety with TypeScript imports.
- Keep one concern per file (projects.json, blogs.json, etc.)
- Export arrays under a named key, not as a root array
- Co-locate TypeScript types in a types/ or lib/ file
- Validate shape at the import level with Zod if data is dynamic
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}“The best project structure is the one your team can navigate without asking questions.”
— A principle worth remembering
None of this is absolute. The structure above reflects what works for my projects. Yours might need something different. The goal is not to follow a template — it is to make intentional decisions early, so you spend less time reorganizing and more time building.
WRITTEN BY
Abdullah Al Maksud
Developer, designer, writer.

