[ LOG.ENTRY // Aug 3, 2025 ]

How I Structure Large Next.js + Node.js Projects

Archive
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:

js
export default function Button({ children, onClick }) { return <button onClick={onClick}>{children}</button>; }

hooks/ Reusable logic lives here.

Example:

js
export 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:

js
export 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)

js
router.post("/login", authController.login);

Controller (handles request/response)

js
exports.login = async (req, res) => { const token = await authService.login(req.body); res.json({ token }); };

Service (business logic)

js
exports.login = async ({ email, password }) => { const user = await userRepository.findByEmail(email); return generateToken(user); };

Repository (database only)

js
exports.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.

#architecture#systemdesign#webdev
All Insights