How to Start a Full Stack MERN Project: Step-by-Step Guide (2026)
Whether you're building a simple to-do app or a production-grade SaaS platform, the MERN stack (MongoDB, Express.js, React, Node.js) is one of the most popular full-stack JavaScript frameworks in the world. In this guide, you'll learn the exact step-by-step process to scaffold, structure, and launch any MERN project — including commands, folder structure, and the most useful libraries for every layer of the stack.
What is the MERN Stack?
The MERN stack is a collection of four JavaScript technologies used to build modern, full-stack web applications:
Layer Technology Role M MongoDB NoSQL database E Express.js Backend web framework R React Frontend UI library N Node.js JavaScript runtime
Because everything is JavaScript (front to back), the MERN stack drastically reduces context switching and allows code sharing between layers.
Prerequisites
Before you start, make sure you have the following installed:
Node.js (v18+) — nodejs.org
npm (comes with Node) or pnpm / yarn
MongoDB — local via MongoDB Community or cloud via MongoDB Atlas
Git — for version control
VS Code — recommended editor
Verify your setup:
node -v # Should print v18.x.x or higher
npm -v # Should print 9.x.x or higher
git --version # Should print git version
Step 1 — Project Initialization
Start by creating a root folder that will house both the backend and frontend.
mkdir my-mern-app
cd my-mern-app
git init
Create a root-level .gitignore:
# .gitignore
node_modules/
.env
dist/
build/
Your project will follow a monorepo-style structure with separate server/ and client/ directories inside one root folder.
Step 2 — Backend Setup (Node.js + Express)
2.1 Initialize the Server
mkdir server
cd server
npm init -y
2.2 Install Core Backend Dependencies
# Core
npm install express mongoose dotenv cors
# Development tools
npm install --save-dev nodemon
What each package does:
Package Purpose express HTTP server & routing framework mongoose MongoDB object data modeling (ODM) dotenv Load environment variables from .env cors Handle Cross-Origin Resource Sharing nodemon Auto-restart server on file changes
2.3 Create the Entry Point
Create server/index.js:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.get('/', (req, res) => {
res.json({ message: 'MERN API is running' });
});
// MongoDB Connection
mongoose
.connect(process.env.MONGO_URI)
.then(() => {
console.log('MongoDB connected');
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
})
.catch((err) => console.error('MongoDB connection error:', err));
2.4 Update package.json Scripts
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
Run the server in dev mode:
npm run dev
Step 3 — MongoDB & Mongoose Setup
3.1 Create a .env File
Inside server/:
PORT=5000
MONGO_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/mydb?retryWrites=true&w=majority
JWT_SECRET=your_super_secret_key
NODE_ENV=development
Tip: Use MongoDB Atlas for a free cloud database. Create a cluster, whitelist your IP, and copy the connection string.
3.2 Create a Mongoose Model
Create server/models/User.js:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: 6,
select: false, // Don't return password by default
},
},
{ timestamps: true }
);
// Hash password before saving
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
module.exports = mongoose.model('User', userSchema);
3.3 Create a Route
Create server/routes/userRoutes.js:
const express = require('express');
const router = express.Router();
const User = require('../models/User');
// GET all users
router.get('/', async (req, res) => {
try {
const users = await User.find();
res.status(200).json(users);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;
Register the route in index.js:
const userRoutes = require('./routes/userRoutes');
app.use('/api/users', userRoutes);
Step 4 — Frontend Setup (React + Vite)
4.1 Scaffold the React App
Go back to the project root:
cd .. # back to my-mern-app/
npm create vite@latest client -- --template react
cd client
npm install
Why Vite over Create React App? Vite is significantly faster for development (instant HMR), lighter, and is now the industry standard.
4.2 Install Core Frontend Dependencies
# Routing
npm install react-router-dom
# HTTP client
npm install axios
# State management
npm install @reduxjs/toolkit react-redux
# Forms & Validation
npm install react-hook-form zod @hookform/resolvers
# UI Component Library (choose one)
npm install @mui/material @emotion/react @emotion/styled # Material UI
# OR
npm install @shadcn/ui # shadcn/ui (Tailwind-based)
# Tailwind CSS (optional but recommended)
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
4.3 Configure Tailwind CSS
In client/tailwind.config.js:
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
In client/src/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
4.4 Configure Vite Proxy
Edit client/vite.config.js to proxy API calls to your backend during development:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
},
},
},
});
This lets you call /api/users from React without CORS issues in development.
Step 5 — Connecting Frontend to Backend
5.1 Create an Axios Instance
Create client/src/api/axios.js:
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
});
// Add auth token to every request automatically
axiosInstance.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default axiosInstance;
5.2 Fetch Data in a React Component
import { useEffect, useState } from 'react';
import axiosInstance from '../api/axios';
const UserList = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUsers = async () => {
try {
const { data } = await axiosInstance.get('/users');
setUsers(data);
} catch (error) {
console.error('Error fetching users:', error);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map((user) => (
<li key={user._id}>{user.name} — {user.email}</li>
))}
</ul>
);
};
export default UserList;
Step 6 — Authentication
6.1 Install Auth Dependencies (Backend)
cd server
npm install jsonwebtoken bcryptjs
6.2 Create Auth Controller
Create server/controllers/authController.js:
const User = require('../models/User');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const generateToken = (id) =>
jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '7d' });
// Register
exports.register = async (req, res) => {
const { name, email, password } = req.body;
try {
const existing = await User.findOne({ email });
if (existing) return res.status(400).json({ message: 'Email already in use' });
const user = await User.create({ name, email, password });
res.status(201).json({ token: generateToken(user._id), user });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
// Login
exports.login = async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.findOne({ email }).select('+password');
if (!user || !(await bcrypt.compare(password, user.password)))
return res.status(401).json({ message: 'Invalid credentials' });
res.json({ token: generateToken(user._id), user });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
6.3 Protect Routes with Middleware
Create server/middleware/authMiddleware.js:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ message: 'Not authorized' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch {
res.status(401).json({ message: 'Token invalid or expired' });
}
};
Step 7 — Recommended Libraries by Category
Backend Libraries
Category Library Install Command Validation express-validator npm i express-validator File Upload multer npm i multer Cloud Storage cloudinary npm i cloudinary Email nodemailer npm i nodemailer Rate Limiting express-rate-limit npm i express-rate-limit Logging morgan npm i morgan Security helmet npm i helmet API Docs swagger-ui-express npm i swagger-ui-express WebSockets socket.io npm i socket.io Caching redis npm i redis Job Queue bull npm i bull Payments stripe npm i stripe
Frontend Libraries
Category Library Install Command Routing react-router-dom npm i react-router-dom HTTP Client axios npm i axios Server State @tanstack/react-query npm i @tanstack/react-query Global State @reduxjs/toolkit npm i @reduxjs/toolkit react-redux Forms react-hook-form npm i react-hook-form Validation Schema zod npm i zod @hookform/resolvers UI Components shadcn/ui See shadcn docs Charts recharts npm i recharts Animations framer-motion npm i framer-motion Notifications react-hot-toast npm i react-hot-toast Icons lucide-react npm i lucide-react Date Handling date-fns npm i date-fns Real-time socket.io-client npm i socket.io-client
Step 8 — Recommended Folder Structure
This structure scales for both small projects and large enterprise apps:
my-mern-app/
│
├── server/ # Backend (Node + Express)
│ ├── config/
│ │ └── db.js # MongoDB connection logic
│ ├── controllers/ # Business logic per resource
│ │ ├── authController.js
│ │ └── userController.js
│ ├── middleware/
│ │ ├── authMiddleware.js # JWT verification
│ │ └── errorMiddleware.js # Global error handler
│ ├── models/ # Mongoose schemas
│ │ └── User.js
│ ├── routes/ # Express routers
│ │ ├── authRoutes.js
│ │ └── userRoutes.js
│ ├── utils/ # Helper functions
│ │ ├── sendEmail.js
│ │ └── generateToken.js
│ ├── uploads/ # Multer file uploads (gitignored)
│ ├── .env # Environment variables (gitignored)
│ ├── .env.example # Template for env vars
│ ├── index.js # Entry point
│ └── package.json
│
├── client/ # Frontend (React + Vite)
│ ├── public/
│ ├── src/
│ │ ├── api/
│ │ │ └── axios.js # Axios base instance
│ │ ├── assets/ # Images, fonts
│ │ ├── components/ # Reusable UI components
│ │ │ ├── ui/ # Generic UI (Button, Modal)
│ │ │ └── layout/ # Header, Footer, Sidebar
│ │ ├── features/ # Feature-based modules
│ │ │ ├── auth/
│ │ │ │ ├── authSlice.js
│ │ │ │ ├── LoginPage.jsx
│ │ │ │ └── RegisterPage.jsx
│ │ │ └── users/
│ │ │ ├── userSlice.js
│ │ │ └── UserList.jsx
│ │ ├── hooks/ # Custom React hooks
│ │ │ └── useAuth.js
│ │ ├── pages/ # Route-level page components
│ │ │ ├── HomePage.jsx
│ │ │ ├── DashboardPage.jsx
│ │ │ └── NotFoundPage.jsx
│ │ ├── store/ # Redux store configuration
│ │ │ └── store.js
│ │ ├── utils/ # Helper functions
│ │ │ └── formatDate.js
│ │ ├── App.jsx # Root component with routes
│ │ ├── main.jsx # React DOM render
│ │ └── index.css # Global styles / Tailwind
│ ├── .env # VITE_ prefixed env variables
│ ├── index.html
│ ├── vite.config.js
│ └── package.json
│
├── .gitignore
└── README.md
Step 9 — Environment Variables
Backend (server/.env)
PORT=5000
NODE_ENV=development
MONGO_URI=mongodb+srv://user:pass@cluster.mongodb.net/dbname
JWT_SECRET=a_very_long_random_secret_string
JWT_EXPIRES_IN=7d
# Email (optional)
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=2525
SMTP_USER=your_user
SMTP_PASS=your_pass
# Cloudinary (optional)
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Frontend (client/.env)
VITE_API_URL=http://localhost:5000/api
Important: In Vite, all environment variables must be prefixed with
VITE_to be exposed to the browser. Access them withimport.meta.env.VITE_API_URL.
Step 10 — Scripts & Running the Project
Run Both Servers Concurrently
Install concurrently at the root level to run both servers with one command:
# From the root of my-mern-app/
npm init -y
npm install --save-dev concurrently
Update root package.json:
{
"scripts": {
"dev": "concurrently \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
"build": "npm run build --prefix client",
"start": "npm start --prefix server"
}
}
Now run everything with:
npm run dev
This starts:
Backend at
http://localhost:5000Frontend at
http://localhost:5173
Step 11 — Deployment
Deploy Backend to Render (Free Tier Available)
Push your code to GitHub
Go to render.com → New Web Service
Connect your GitHub repo, point to the
server/directorySet your environment variables in the Render dashboard
Build command:
npm install| Start command:node index.js
Deploy Frontend to Vercel
cd client
npm run build
Go to vercel.com → Import Project
Set root directory to
client/Build command:
npm run build| Output:distAdd your environment variable
VITE_API_URLpointing to your Render backend URL
Alternative Deployment Options
Platform Backend Frontend Railway Yes - Excellent Yes - Possible Heroku Yes - Classic No Fly.io Yes - Docker-based No Netlify No Yes - Excellent Vercel Yes - Serverless Yes - Best-in-class AWS EC2 Yes - Full control Yes - Full control
Final Tips
For Small Projects:
Skip Redux; use React Context or Zustand (
npm i zustand) insteadUse MongoDB Atlas free tier (512MB is enough to start)
Keep everything in a single Express
index.jsuntil you need to split it
For Advanced / Production Projects:
Add
helmetfor HTTP security headers:app.use(helmet())Add
express-rate-limitto prevent abuseUse
@tanstack/react-queryfor data fetching, caching, and synchronizationAdd a CI/CD pipeline (GitHub Actions) to auto-deploy on push
Write tests with
Jest+Supertest(backend) andVitest+React Testing Library(frontend)Use TypeScript for large teams — create vite app with
--template react-ts
Useful VS Code Extensions:
ESLint
Prettier
Thunder Client (API testing inside VS Code)
MongoDB for VS Code
GitLens
Summary
Here's the complete quick-start checklist:
[ ] Create root folder & init git
[ ]
server/—npm init, install Express + Mongoose + dotenv + cors[ ] Create
index.js, models, routes, controllers[ ] Set up
.envwithMONGO_URIandJWT_SECRET[ ]
client/— scaffold with Vite, install axios + react-router-dom[ ] Configure Vite proxy to backend
[ ] Add
concurrentlyto root for one-command dev startup[ ] Add auth (JWT + bcrypt)
[ ] Build features using feature-based folder structure
[ ] Deploy backend to Render, frontend to Vercel
The MERN stack gives you a powerful, unified JavaScript ecosystem to build anything from a weekend side project to a scalable production application. Follow this structure and you will spend less time on boilerplate and more time building actual features.