How I Structure Large Next.js + Node.js Projects
Why Project Structure Matters
When your codebase grows, structure is what keeps everything readable and scalable. A good structure makes debugging easier, collaboration smoother, and new features safer to build.
Your goal should be: predictable, modular, and easy to navigate.
Visual Overview of the Full Project
Here is a high-level view of how I separate frontend and backend:
my-project/
│
├── client/ # Next.js frontend
│ └── ...
│
└── server/ # Node.js + Express backend
└── ...
This clear separation prevents mixing concerns and keeps both sides independent.
Next.js Frontend Structure (Visual + Example)
Recommended structure:
client/
│
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ └── register/
│ ├── dashboard/
│ └── layout.js
│
├── components/
│ ├── ui/
│ │ ├── Button.js
│ │ ├── Card.js
│ │ └── Modal.js
│ └── Navbar.js
│
├── hooks/
│ ├── useAuth.js
│ └── useDebounce.js
│
├── lib/
│ ├── formatDate.js
│ └── constants.js
│
└── services/
└── api.js
What each folder does
app/ All routes and layouts live here. I group related routes instead of scattering files.
Example:
app/dashboard/page.js
components/ Pure UI components only.
Example Button component:
jsexport default function Button({ children, onClick }) { return <button onClick={onClick}>{children}</button>; }
hooks/ Reusable logic lives here.
Example:
jsexport function useDebounce(value, delay) { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debounced; }
services/ All API calls are centralized.
Example:
jsexport async function getUser() { const res = await fetch("/api/user"); return res.json(); }
Node.js + Express Backend Structure (Visual + Example)
Recommended structure:
server/
│
├── routes/
│ └── auth.routes.js
│
├── controllers/
│ └── auth.controller.js
│
├── services/
│ └── auth.service.js
│
├── repositories/
│ └── user.repository.js
│
├── models/
│ └── User.js
│
└── middleware/
└── auth.middleware.js
How this works in practice
Route (only defines endpoints)
jsrouter.post("/login", authController.login);
Controller (handles request/response)
jsexports.login = async (req, res) => { const token = await authService.login(req.body); res.json({ token }); };
Service (business logic)
jsexports.login = async ({ email, password }) => { const user = await userRepository.findByEmail(email); return generateToken(user); };
Repository (database only)
jsexports.findByEmail = async (email) => { return User.findOne({ email }); };
Clear Flow Diagram
Frontend → API Service → Express Route → Controller → Service → Repository → Database
Next.js UI
↓
services/api.js
↓
Express Route
↓
Controller
↓
Service
↓
Repository
↓
MongoDB
This keeps everything clean and testable.
Separating Business Logic from UI
Bad pattern:
js// Inside React component ❌ const handleLogin = async () => { const res = await fetch("/login"); const data = await res.json(); };
Better pattern:
js// api.js ✅ export const login = async (data) => { const res = await fetch("/login", { body: JSON.stringify(data) }); return res.json(); };
Your components should only display data, not process it.
Sharing Logic Between Frontend and Backend
If both sides need the same validation or formatting, keep it in a shared utility.
Example:
shared/
└── validateEmail.js
Common Mistakes to Avoid
Putting everything inside one folder Calling APIs directly everywhere Mixing UI and business logic No clear authentication pattern Skipping middleware for security
Final Takeaway
Good structure is not about perfection. It is about consistency. If you follow this pattern, your Next.js + Node.js projects will stay clean, scalable, and easy to maintain.