In this comprehensive guide, we'll walk through building a complete e-commerce web application from scratch using the MERN stack (MongoDB, Express.js, React, Node.js). By the end of this tutorial, you'll have a fully functional online store with product listings, shopping cart functionality, user authentication, and payment integration.
🛍️ What We'll Build
Our e-commerce app will include:
- Product catalog with categories and search
- User authentication and authorization
- Shopping cart management
- Order processing
- Payment integration with Stripe
- Admin dashboard for product management
- Responsive design for mobile and desktop
🛠️ Prerequisites
Before we start, make sure you have:
- Node.js (v16 or higher) installed
- MongoDB installed or MongoDB Atlas account
- Basic knowledge of JavaScript, React, and Node.js
- Code editor (VS Code recommended)
- Postman for API testing
📁 Project Structure
ecommerce-app/
├── client/ # React frontend
│ ├── public/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── context/
│ │ ├── services/
│ │ └── utils/
│ └── package.json
├── server/ # Node.js backend
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── middleware/
│ ├── config/
│ └── package.json
└── README.md
🚀 Step 1: Setting Up the Backend
Initialize the Server
mkdir ecommerce-app
cd ecommerce-app
mkdir server
cd server
npm init -y
Install Dependencies
npm install express mongoose cors dotenv bcryptjs jsonwebtoken
npm install stripe nodemailer multer
npm install -D nodemon concurrently
Create Server Structure
// 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());
// Database Connection
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/ecommerce', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
// Routes
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));
app.use('/api/orders', require('./routes/orders'));
app.use('/api/payment', require('./routes/payment'));
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Create User Model
// server/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
isAdmin: { type: Boolean, default: false },
address: {
street: String,
city: String,
zipCode: String,
country: String,
},
}, { timestamps: true });
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
userSchema.methods.comparePassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
module.exports = mongoose.model('User', userSchema);
Create Product Model
// server/models/Product.js
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
name: { type: String, required: true },
description: { type: String, required: true },
price: { type: Number, required: true },
category: { type: String, required: true },
brand: { type: String, required: true },
image: { type: String, required: true },
countInStock: { type: Number, required: true, default: 0 },
rating: { type: Number, default: 0 },
numReviews: { type: Number, default: 0 },
reviews: [{
name: String,
rating: Number,
comment: String,
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
}],
}, { timestamps: true });
module.exports = mongoose.model('Product', productSchema);
Create Order Model
// server/models/Order.js
const mongoose = require('mongoose');
const orderSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
orderItems: [{
name: { type: String, required: true },
qty: { type: Number, required: true },
image: { type: String, required: true },
price: { type: Number, required: true },
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
}],
shippingAddress: {
address: { type: String, required: true },
city: { type: String, required: true },
postalCode: { type: String, required: true },
country: { type: String, required: true },
},
paymentMethod: { type: String, required: true },
paymentResult: {
id: String,
status: String,
update_time: String,
email_address: String,
},
itemsPrice: { type: Number, required: true },
taxPrice: { type: Number, required: true },
shippingPrice: { type: Number, required: true },
totalPrice: { type: Number, required: true },
isPaid: { type: Boolean, default: false },
paidAt: Date,
isDelivered: { type: Boolean, default: false },
deliveredAt: Date,
}, { timestamps: true });
module.exports = mongoose.model('Order', orderSchema);
Create Authentication Routes
// server/routes/users.js
const express = require('express');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = require('../middleware/auth');
const router = express.Router();
// Register User
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
const userExists = await User.findOne({ email });
if (userExists) {
return res.status(400).json({ message: 'User already exists' });
}
const user = await User.create({ name, email, password });
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: '30d',
});
res.status(201).json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
token,
});
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Login User
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ message: 'Invalid email or password' });
}
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: '30d',
});
res.json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
token,
});
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Get User Profile
router.get('/profile', auth, async (req, res) => {
try {
const user = await User.findById(req.user.id);
if (user) {
res.json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
address: user.address,
});
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;
Create Product Routes
// server/routes/products.js
const express = require('express');
const Product = require('../models/Product');
const auth = require('../middleware/auth');
const admin = require('../middleware/admin');
const router = express.Router();
// Get All Products
router.get('/', async (req, res) => {
try {
const pageSize = 10;
const page = Number(req.query.pageNumber) || 1;
const keyword = req.query.keyword
? {
name: {
$regex: req.query.keyword,
$options: 'i',
},
}
: {};
const count = await Product.countDocuments({ ...keyword });
const products = await Product.find({ ...keyword })
.limit(pageSize)
.skip(pageSize * (page - 1));
res.json({ products, page, pages: Math.ceil(count / pageSize) });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Get Single Product
router.get('/:id', async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (product) {
res.json(product);
} else {
res.status(404).json({ message: 'Product not found' });
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Create Product (Admin Only)
router.post('/', auth, admin, async (req, res) => {
try {
const product = new Product({
name: 'Sample Product',
price: 0,
user: req.user._id,
image: '/images/sample.jpg',
brand: 'Sample Brand',
category: 'Sample Category',
countInStock: 0,
numReviews: 0,
description: 'Sample description',
});
const createdProduct = await product.save();
res.status(201).json(createdProduct);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Update Product (Admin Only)
router.put('/:id', auth, admin, async (req, res) => {
try {
const { name, price, description, image, brand, category, countInStock } = req.body;
const product = await Product.findById(req.params.id);
if (product) {
product.name = name;
product.price = price;
product.description = description;
product.image = image;
product.brand = brand;
product.category = category;
product.countInStock = countInStock;
const updatedProduct = await product.save();
res.json(updatedProduct);
} else {
res.status(404).json({ message: 'Product not found' });
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Delete Product (Admin Only)
router.delete('/:id', auth, admin, async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (product) {
await product.remove();
res.json({ message: 'Product removed' });
} else {
res.status(404).json({ message: 'Product not found' });
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Create Product Review
router.post('/:id/reviews', auth, async (req, res) => {
try {
const { rating, comment } = req.body;
const product = await Product.findById(req.params.id);
if (product) {
const alreadyReviewed = product.reviews.find(
(r) => r.user.toString() === req.user._id.toString()
);
if (alreadyReviewed) {
return res.status(400).json({ message: 'Product already reviewed' });
}
const review = {
name: req.user.name,
rating: Number(rating),
comment,
user: req.user._id,
};
product.reviews.push(review);
product.numReviews = product.reviews.length;
product.rating = product.reviews.reduce((acc, item) => item.rating + acc, 0) / product.reviews.length;
await product.save();
res.status(201).json({ message: 'Review added' });
} else {
res.status(404).json({ message: 'Product not found' });
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;
🎨 Step 2: Setting Up the Frontend
Initialize React App
cd ..
npx create-react-app client
cd client
Install Frontend Dependencies
npm install axios react-router-dom react-bootstrap bootstrap
npm install @reduxjs/toolkit react-redux react-toastify
Create Redux Store
// client/src/store.js
import { configureStore } from '@reduxjs/toolkit';
import productSlice from './slices/productSlice';
import cartSlice from './slices/cartSlice';
import userSlice from './slices/userSlice';
export const store = configureStore({
reducer: {
products: productSlice,
cart: cartSlice,
user: userSlice,
},
});
Create Product Slice
// client/src/slices/productSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const initialState = {
products: [],
product: {},
loading: false,
error: null,
page: 1,
pages: 1,
};
export const listProducts = createAsyncThunk(
'products/listProducts',
async ({ keyword = '', pageNumber = 1 }) => {
const { data } = await axios.get(
`/api/products?keyword=${keyword}&pageNumber=${pageNumber}`
);
return data;
}
);
export const listProductDetails = createAsyncThunk(
'products/listProductDetails',
async (id) => {
const { data } = await axios.get(`/api/products/${id}`);
return data;
}
);
const productSlice = createSlice({
name: 'products',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(listProducts.pending, (state) => {
state.loading = true;
})
.addCase(listProducts.fulfilled, (state, action) => {
state.loading = false;
state.products = action.payload.products;
state.page = action.payload.page;
state.pages = action.payload.pages;
})
.addCase(listProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
.addCase(listProductDetails.pending, (state) => {
state.loading = true;
})
.addCase(listProductDetails.fulfilled, (state, action) => {
state.loading = false;
state.product = action.payload;
})
.addCase(listProductDetails.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});
export default productSlice.reducer;
Create Cart Slice
// client/src/slices/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
cartItems: localStorage.getItem('cartItems')
? JSON.parse(localStorage.getItem('cartItems'))
: [],
shippingAddress: localStorage.getItem('shippingAddress')
? JSON.parse(localStorage.getItem('shippingAddress'))
: {},
paymentMethod: localStorage.getItem('paymentMethod')
? localStorage.getItem('paymentMethod')
: '',
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addToCart: (state, action) => {
const item = action.payload;
const existItem = state.cartItems.find((x) => x.product === item.product);
if (existItem) {
state.cartItems = state.cartItems.map((x) =>
x.product === existItem.product ? item : x
);
} else {
state.cartItems = [...state.cartItems, item];
}
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
},
removeFromCart: (state, action) => {
state.cartItems = state.cartItems.filter((x) => x.product !== action.payload);
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
},
saveShippingAddress: (state, action) => {
state.shippingAddress = action.payload;
localStorage.setItem('shippingAddress', JSON.stringify(action.payload));
},
savePaymentMethod: (state, action) => {
state.paymentMethod = action.payload;
localStorage.setItem('paymentMethod', action.payload);
},
clearCartItems: (state) => {
state.cartItems = [];
localStorage.setItem('cartItems', JSON.stringify([]));
},
},
});
export const {
addToCart,
removeFromCart,
saveShippingAddress,
savePaymentMethod,
clearCartItems,
} = cartSlice.actions;
export default cartSlice.reducer;
Create Product Components
// client/src/components/Product.jsx
import React from 'react';
import { Card, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import Rating from './Rating';
const Product = ({ product }) => {
return (
<Card className="my-3 p-3 rounded">
<Link to={`/product/${product._id}`}>
<Card.Img src={product.image} variant="top" />
</Link>
<Card.Body>
<Link to={`/product/${product._id}`}>
<Card.Title as="div">
<strong>{product.name}</strong>
</Card.Title>
</Link>
<Card.Text as="div">
<Rating
value={product.rating}
text={`${product.numReviews} reviews`}
/>
</Card.Text>
<Card.Text as="h3">${product.price}</Card.Text>
</Card.Body>
</Card>
);
};
export default Product;
// client/src/components/Rating.jsx
import React from 'react';
import { FaStar, FaStarHalfAlt, FaRegStar } from 'react-icons/fa';
const Rating = ({ value, text, color }) => {
return (
<div className="rating">
<span>
{value >= 1 ? (
<FaStar />
) : value >= 0.5 ? (
<FaStarHalfAlt />
) : (
<FaRegStar />
)}
</span>
<span>
{value >= 2 ? (
<FaStar />
) : value >= 1.5 ? (
<FaStarHalfAlt />
) : (
<FaRegStar />
)}
</span>
<span>
{value >= 3 ? (
<FaStar />
) : value >= 2.5 ? (
<FaStarHalfAlt />
) : (
<FaRegStar />
)}
</span>
<span>
{value >= 4 ? (
<FaStar />
) : value >= 3.5 ? (
<FaStarHalfAlt />
) : (
<FaRegStar />
)}
</span>
<span>
{value >= 5 ? (
<FaStar />
) : value >= 4.5 ? (
<FaStarHalfAlt />
) : (
<FaRegStar />
)}
</span>
<span>{text && text}</span>
</div>
);
};
export default Rating;
Create Product List Page
// client/src/pages/HomePage.jsx
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Row, Col } from 'react-bootstrap';
import Product from '../components/Product';
import { listProducts } from '../slices/productSlice';
import Loader from '../components/Loader';
import Message from '../components/Message';
const HomePage = () => {
const dispatch = useDispatch();
const { loading, error, products } = useSelector((state) => state.products);
useEffect(() => {
dispatch(listProducts());
}, [dispatch]);
return (
<>
<h1>Latest Products</h1>
{loading ? (
<Loader />
) : error ? (
<Message variant="danger">{error}</Message>
) : (
<Row>
{products.map((product) => (
<Col key={product._id} sm={12} md={6} lg={4} xl={3}>
<Product product={product} />
</Col>
))}
</Row>
)}
</>
);
};
export default HomePage;
Create Shopping Cart Page
// client/src/pages/CartPage.jsx
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { Row, Col, ListGroup, Image, Form, Button, Card } from 'react-bootstrap';
import { FaTrash } from 'react-icons/fa';
import { addToCart, removeFromCart } from '../slices/cartSlice';
const CartPage = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
const addToCartHandler = (product, qty) => {
dispatch(addToCart({ ...product, qty }));
};
const removeFromCartHandler = (id) => {
dispatch(removeFromCart(id));
};
const checkoutHandler = () => {
navigate('/login?redirect=/shipping');
};
return (
<Row>
<Col md={8}>
<h1>Shopping Cart</h1>
{cartItems.length === 0 ? (
<Message>
Your cart is empty <Link to="/">Go Back</Link>
</Message>
) : (
<ListGroup variant="flush">
{cartItems.map((item) => (
<ListGroup.Item key={item.product}>
<Row>
<Col md={2}>
<Image src={item.image} alt={item.name} fluid rounded />
</Col>
<Col md={3}>
<Link to={`/product/${item.product}`}>{item.name}</Link>
</Col>
<Col md={2}>${item.price}</Col>
<Col md={2}>
<Form.Control
as="select"
value={item.qty}
onChange={(e) =>
addToCartHandler(item, Number(e.target.value))
}
>
{[...Array(item.countInStock).keys()].map((x) => (
<option key={x + 1} value={x + 1}>
{x + 1}
</option>
))}
</Form.Control>
</Col>
<Col md={2}>
<Button
type="button"
variant="light"
onClick={() => removeFromCartHandler(item.product)}
>
<FaTrash />
</Button>
</Col>
</Row>
</ListGroup.Item>
))}
</ListGroup>
)}
</Col>
<Col md={4}>
<Card>
<ListGroup variant="flush">
<ListGroup.Item>
<h2>
Subtotal ({cartItems.reduce((acc, item) => acc + item.qty, 0)}) items
</h2>
$
{cartItems
.reduce((acc, item) => acc + item.qty * item.price, 0)
.toFixed(2)}
</ListGroup.Item>
<ListGroup.Item>
<Button
type="button"
className="btn-block"
disabled={cartItems.length === 0}
onClick={checkoutHandler}
>
Proceed To Checkout
</Button>
</ListGroup.Item>
</ListGroup>
</Card>
</Col>
</Row>
);
};
export default CartPage;
💳 Step 3: Payment Integration with Stripe
Install Stripe Dependencies
npm install stripe @stripe/stripe-js
Create Payment Route
// server/routes/payment.js
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const router = express.Router();
router.post('/create-checkout-session', async (req, res) => {
try {
const { cartItems } = req.body;
const line_items = cartItems.map((item) => ({
price_data: {
currency: 'usd',
product_data: {
name: item.name,
images: [item.image],
},
unit_amount: item.price * 100,
},
quantity: item.qty,
}));
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items,
mode: 'payment',
success_url: `${process.env.CLIENT_URL}/success`,
cancel_url: `${process.env.CLIENT_URL}/cart`,
});
res.json({ id: session.id });
} catch (error) {
res.status(500).json({ message: error.message });
}
});
module.exports = router;
Create Payment Component
// client/src/components/CheckoutForm.jsx
import React from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import CheckoutForm from './CheckoutForm';
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);
const Payment = () => {
return (
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
);
};
export default Payment;
🚀 Step 4: Deployment
Environment Variables
Create .env files for both client and server:
# server/.env
MONGODB_URI=mongodb://localhost:27017/ecommerce
JWT_SECRET=your_jwt_secret
STRIPE_SECRET_KEY=your_stripe_secret_key
CLIENT_URL=http://localhost:3000
# client/.env
REACT_APP_API_URL=http://localhost:5000
REACT_APP_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
Deploy to Heroku
# Install Heroku CLI
npm install -g heroku
# Login to Heroku
heroku login
# Create Heroku app
heroku create your-ecommerce-app
# Set environment variables
heroku config:set MONGODB_URI=your_mongodb_atlas_uri
heroku config:set JWT_SECRET=your_jwt_secret
heroku config:set STRIPE_SECRET_KEY=your_stripe_secret_key
# Deploy
git subtree push --prefix server heroku main
🎉 Conclusion
Congratulations! You've successfully built a full-featured e-commerce application using the MERN stack. This application includes:
✅ User authentication and authorization
✅ Product management with categories and search
✅ Shopping cart functionality
✅ Order processing
✅ Payment integration with Stripe
✅ Responsive design
✅ Admin dashboard capabilities
📚 Next Steps
To enhance your e-commerce application, consider adding:
- Real-time inventory management
- Advanced search and filtering
- Customer reviews and ratings
- Email notifications
- Analytics dashboard
- Social media integration
- Multi-language support
- Progressive Web App (PWA) features
🛠️ Technologies Used
- MongoDB: NoSQL database for storing products, users, and orders
- Express.js: Backend framework for RESTful APIs
- React: Frontend library for building user interfaces
- Node.js: JavaScript runtime for server-side development
- Redux: State management for the React application
- Stripe: Payment processing platform
- Bootstrap: CSS framework for responsive design
- JWT: Authentication tokens for secure user sessions
📝 Final Thoughts
Building an e-commerce application with the MERN stack provides a solid foundation for understanding full-stack web development. The skills you've learned here are transferable to many other types of web applications.
Remember to always prioritize security, performance, and user experience when building production applications. Happy coding! 🚀

Top comments (0)