With Node.js, MongoDB, and VS Code installed, you are ready to scaffold the actual MERN project. A clean, well-organised project structure is not a luxury โ it determines how easily you can add features, debug problems, and hand the project to another developer. In this lesson you will create the complete monorepo folder structure with separate client and server directories, initialise both npm projects, install the core dependencies for each layer, write the first working Express server, scaffold the React frontend with Vite, and run both together for the first time.
MERN Project Architecture Decision
| Approach | Structure | Best For |
|---|---|---|
| Monorepo (recommended) | One root folder containing client/ and server/ |
Small to medium projects, solo developers, this series |
| Separate repositories | Two completely independent Git repos | Large teams where frontend and backend are deployed independently |
| Fullstack in one Express app | Express serves React’s dist/ build files |
Simple single-server deployment โ used in the deployment chapter |
client and server each have their own package.json and node_modules. They are completely independent npm projects that happen to live in the same Git repository. During development you run them on different ports โ the React Vite dev server on port 5173 and the Express API on port 5000.package.json with convenience scripts that start both servers with one command using the concurrently package: npm run dev at the project root starts both the Express server and the Vite dev server simultaneously, with colour-coded output for each in the same terminal.cors middleware to Express (app.use(cors())) and optionally configure a Vite proxy so that /api requests are forwarded to Express automatically. Both are covered in the scaffold below.Complete Scaffold โ Step by Step
# โโ Step 1: Create the project root โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
mkdir mern-blog
cd mern-blog
git init
echo "node_modules/\n.env\ndist/" > .gitignore
# โโ Step 2: Scaffold the React client with Vite โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
npm create vite@latest client -- --template react
cd client
npm install
npm install axios react-router-dom
# โโ Step 3: Scaffold the Express server โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
cd ..
mkdir server
cd server
npm init -y
# Core production dependencies
npm install express mongoose dotenv cors helmet bcryptjs jsonwebtoken nodemailer multer
# Development dependencies
npm install -D nodemon
# โโ Step 4: Back to root โ add concurrently for running both together โโโโโโโโโ
cd ..
npm init -y
npm install -D concurrently
Full Project Structure
mern-blog/
โโโ .gitignore โ covers both client and server
โโโ package.json โ root scripts (start both servers)
โ
โโโ client/ โ React + Vite (port 5173)
โ โโโ public/
โ โโโ src/
โ โ โโโ components/ โ reusable UI components
โ โ โโโ pages/ โ page-level components (routes)
โ โ โโโ services/ โ Axios API call functions
โ โ โโโ context/ โ React Context providers
โ โ โโโ hooks/ โ custom hooks
โ โ โโโ App.jsx
โ โ โโโ main.jsx
โ โโโ vite.config.js โ proxy config: /api โ localhost:5000
โ โโโ package.json
โ
โโโ server/ โ Express + Node.js (port 5000)
โโโ index.js โ entry point
โโโ .env โ secrets (never commit)
โโโ src/
โ โโโ config/
โ โ โโโ db.js โ Mongoose connection
โ โโโ models/
โ โ โโโ Post.js
โ โ โโโ User.js
โ โโโ routes/
โ โ โโโ posts.js
โ โ โโโ users.js
โ โ โโโ auth.js
โ โโโ controllers/
โ โ โโโ postController.js
โ โ โโโ authController.js
โ โโโ middleware/
โ โโโ auth.js
โ โโโ errorHandler.js
โโโ package.json
Express Server Entry Point
// server/index.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const connectDB = require('./src/config/db');
const app = express();
const PORT = process.env.PORT || 5000;
// โโ Connect to MongoDB โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
connectDB();
// โโ Middleware โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(helmet());
app.use(cors({ origin: process.env.CLIENT_URL || 'http://localhost:5173' }));
app.use(express.json());
// โโ Routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use('/api/posts', require('./src/routes/posts'));
app.use('/api/auth', require('./src/routes/auth'));
// โโ Health check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', environment: process.env.NODE_ENV });
});
// โโ Global error handler โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({ success: false, message: err.message });
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
Mongoose Connection Module
// server/src/config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI);
console.log(`MongoDB connected: ${conn.connection.host}`);
} catch (err) {
console.error(`MongoDB connection error: ${err.message}`);
process.exit(1); // exit with failure โ cannot run without a database
}
};
module.exports = connectDB;
Vite Proxy Configuration
// client/vite.config.js โ proxy /api requests to Express in development
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
},
},
},
});
Root package.json โ Run Both Servers at Once
{
"name": "mern-blog",
"scripts": {
"dev": "concurrently \"npm run server\" \"npm run client\"",
"server": "cd server && npm run dev",
"client": "cd client && npm run dev",
"install-all": "npm install && cd server && npm install && cd ../client && npm install"
},
"devDependencies": {
"concurrently": "^8.2.2"
}
}
Environment Variables
# server/.env โ copy this template, fill in your values
PORT=5000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/blogdb
JWT_SECRET=change_this_to_a_long_random_string_in_production
JWT_EXPIRES_IN=7d
CLIENT_URL=http://localhost:5173
EMAIL_USER=your_email@gmail.com
EMAIL_PASS=your_gmail_app_password
Common Mistakes
Mistake 1 โ Running npm install in the wrong directory
โ Wrong โ installing server packages from the project root or client directory:
cd mern-blog
npm install express # installs express in root node_modules โ not in server/
โ
Correct โ always cd into the correct sub-project first:
cd mern-blog/server
npm install express # correctly installed in server/node_modules/
Mistake 2 โ Forgetting the .env file after cloning
โ Wrong โ cloning the repo and running the server without creating .env:
node index.js
# process.env.MONGODB_URI is undefined โ mongoose.connect(undefined) โ error
โ
Correct โ always include a .env.example file in the repo with placeholder values, so new developers know exactly what variables to set:
# server/.env.example โ this file IS committed to Git
PORT=5000
MONGODB_URI=mongodb://localhost:27017/your_db_name
JWT_SECRET=replace_with_random_string
Mistake 3 โ CORS error when React calls Express
โ Wrong โ Express running without the cors middleware:
Access to fetch at 'http://localhost:5000/api/posts' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header
โ Correct โ add cors middleware before routes in Express, or use the Vite proxy to avoid CORS entirely in development:
app.use(cors({ origin: 'http://localhost:5173' })); // before app.use('/api/...')
Quick Reference
| Task | Command |
|---|---|
| Scaffold React + Vite | npm create vite@latest client -- --template react |
| Init Express project | mkdir server && cd server && npm init -y |
| Install server deps | npm install express mongoose dotenv cors helmet |
| Install nodemon | npm install -D nodemon |
| Start server (dev) | npm run dev (uses nodemon) |
| Start React (dev) | npm run dev in client/ |
| Start both at once | npm run dev in project root (uses concurrently) |