A well-organised project structure is not a cosmetic concern โ it determines how fast you can add features, how easily a new developer can understand the codebase, and how clearly responsibilities are separated across files. By the time you finish this chapter, your MERN Blog server will be a fully working REST API backed by MongoDB. In this lesson you will design the complete folder structure that will hold all of that code โ the models, routes, controllers, middleware, config, and utilities โ and understand the responsibility of each layer.
The Complete Server Folder Structure
server/
โโโ index.js โ Entry point: loads env, connects DB, starts HTTP server
โโโ .env โ Secrets (never commit)
โโโ .env.example โ Template with placeholder values (commit this)
โโโ .gitignore
โโโ nodemon.json
โโโ package.json
โ
โโโ src/
โโโ config/
โ โโโ db.js โ Mongoose connect/disconnect module
โ
โโโ models/
โ โโโ User.js โ User schema + model
โ โโโ Post.js โ Post schema + model
โ โโโ Comment.js โ Comment schema + model
โ
โโโ controllers/
โ โโโ authController.js โ register, login, logout, getMe, forgotPassword...
โ โโโ postController.js โ getAllPosts, getPostById, createPost, updatePost, deletePost
โ โโโ userController.js โ getUserProfile, updateProfile, deleteAccount
โ โโโ commentController.js โ getComments, createComment, updateComment, deleteComment
โ
โโโ routes/
โ โโโ auth.js โ Express Router for /api/auth/*
โ โโโ posts.js โ Express Router for /api/posts/*
โ โโโ users.js โ Express Router for /api/users/*
โ โโโ comments.js โ Express Router for /api/posts/:id/comments/*
โ
โโโ middleware/
โ โโโ auth.js โ JWT protect middleware
โ โโโ authorize.js โ Role-based access control factory
โ โโโ validate.js โ express-validator error collector
โ โโโ validateObjectId.js โ MongoDB ObjectId format checker
โ โโโ notFound.js โ 404 catch-all
โ โโโ errorHandler.js โ Global error handler (4 args)
โ
โโโ validators/
โ โโโ authValidators.js โ register and login validation rules
โ โโโ postValidators.js โ createPost and updatePost validation rules
โ
โโโ utils/
โโโ AppError.js โ Custom error class with statusCode
โโโ asyncHandler.js โ Async route wrapper for Express 4
โโโ sendEmail.js โ Nodemailer email sending utility
req, calls Mongoose methods, and writes to res is easy to unit test โ you can call it with a mock req object without needing a running Express server. The less your controller knows about Express routing, the more portable and testable it is.The Entry Point โ index.js
// server/index.js โ clean, minimal entry point
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const connectDB = require('./src/config/db');
const app = express();
const PORT = process.env.PORT || 5000;
// โโ Security and utility middleware โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(helmet());
app.use(cors({
origin: process.env.CLIENT_URL || 'http://localhost:5173',
credentials: true,
}));
if (process.env.NODE_ENV === 'development') app.use(morgan('dev'));
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 200 }));
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: false }));
// โโ API Routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use('/api/auth', require('./src/routes/auth'));
app.use('/api/posts', require('./src/routes/posts'));
app.use('/api/users', require('./src/routes/users'));
// โโ Health check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', env: process.env.NODE_ENV });
});
// โโ Error handling (always last) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(require('./src/middleware/notFound'));
app.use(require('./src/middleware/errorHandler'));
// โโ Connect DB then start server โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const start = async () => {
await connectDB();
app.listen(PORT, () => console.log(`Server โ http://localhost:${PORT}`));
};
start().catch(err => { console.error(err); process.exit(1); });
Responsibility Mapping
| Layer | File(s) | Responsibility | Knows About |
|---|---|---|---|
| Config | db.js | Database connection lifecycle | mongoose, process.env |
| Model | Post.js, User.js | Schema, validation, hooks, methods | mongoose only |
| Controller | postController.js | Business logic, DB queries, response | Models, AppError, asyncHandler |
| Route | posts.js | URL-to-handler mapping, middleware chain | express.Router, middleware, controllers |
| Middleware | auth.js, errorHandler.js | Cross-cutting concerns โ auth, errors | JWT, AppError, mongoose error types |
| Validator | postValidators.js | Input validation rules | express-validator |
| Utility | AppError.js, asyncHandler.js | Reusable helpers | nothing app-specific |
Common Mistakes
Mistake 1 โ Putting Mongoose queries directly in route files
โ Wrong โ routes doing database work:
// routes/posts.js
router.get('/', async (req, res) => {
const posts = await Post.find({ published: true }).populate('author');
res.json({ data: posts });
});
// Route file now has business logic โ hard to test, hard to reuse
โ Correct โ route files stay thin; all logic is in controller files:
// routes/posts.js
router.get('/', getAllPosts); // โ just the mapping
// controllers/postController.js
const getAllPosts = asyncHandler(async (req, res) => {
const posts = await Post.find({ published: true }).populate('author');
res.json({ data: posts });
});
Mistake 2 โ Importing models in route files instead of controllers
โ Wrong โ routes importing models directly:
// routes/posts.js
const Post = require('../models/Post'); // model imported into route file
router.get('/', async (req, res) => { const posts = await Post.find()... });
โ Correct โ only controller files import models:
// controllers/postController.js
const Post = require('../models/Post'); // model belongs in controller โ
Mistake 3 โ Inconsistent import paths caused by poor folder structure
โ Wrong โ deep relative paths that break when files are moved:
const AppError = require('../../../utils/AppError'); // fragile relative path
โ Correct โ keep the structure flat (max 2 levels deep in src/) and consistent:
const AppError = require('../utils/AppError'); // one level up from controllers/ โ
Quick Reference โ File Creation Checklist
| When You Need To | Create / Edit |
|---|---|
| Add a new resource (e.g. comments) | models/Comment.js, controllers/commentController.js, routes/comments.js |
| Add a new route to an existing resource | routes/posts.js + controllers/postController.js |
| Add input validation for a route | validators/postValidators.js + routes/posts.js (add validate middleware) |
| Add a reusable utility | utils/yourUtil.js |
| Add a new middleware | middleware/yourMiddleware.js |
| Add a new environment variable | .env (add value) + .env.example (add placeholder) |