Creating Your First MERN Project Structure

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
Note: In the monorepo setup, 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.
Tip: Add a root-level 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.
Warning: During development, React (port 5173) and Express (port 5000) run on different origins โ€” this triggers the browser’s CORS policy and blocks API requests from React. You must add the 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)

🧠 Test Yourself

In a MERN monorepo, your React app (port 5173) calls fetch('/api/posts') but receives a CORS error. You have already added app.use(cors()) to Express. What else is likely missing?