Build a Complete MERN Stack CRUD App with Image Upload (Step-by-Step)
This is a complete step-by-step guide to building a MERN Stack CRUD application with Image Upload using Multer. Let's dive right into the structure and code!
📂 The Complete Folder Structure
mern-crud-app/
│
├── backend/
│ ├── models/
│ │ └── Employee.js
│ ├── routes/
│ │ └── employeeRoutes.js
│ ├── uploads/ (Make sure to create this empty folder)
│ ├── .env
│ └── server.js
│
└── frontend/
└── src/
├── components/
│ └── EmployeeManager.jsx
├── App.jsx
└── index.css
⚙️ Part 1: Backend Code
1. backend/.env
PORT=5000
MONGO_URI=mongodb://127.0.0.1:27017/mock-crud
2. backend/server.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
// Middlewares
app.use(cors());
app.use(express.json());
// Setup static folder for frontend image access
app.use('/uploads', express.static('uploads'));
// Routes
const employeeRoutes = require('./routes/employeeRoutes');
app.use('/api/employees', employeeRoutes);
// MongoDB Connection
mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('MongoDB Connected Successfully! ✅'))
.catch((err) => console.log('MongoDB Connection Failed ❌', err));
app.get('/', (req, res) => {
res.send('Backend Server is Running! 🚀');
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
3. backend/models/Employee.js
const mongoose = require('mongoose');
const employeeSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
phone: { type: String, required: true },
designation: { type: String, required: true },
experience: { type: Number, required: true },
salary: { type: Number, required: true },
image: { type: String, required: true }
}, { timestamps: true });
module.exports = mongoose.model('Employee', employeeSchema);
4. backend/routes/employeeRoutes.js
const express = require('express');
const multer = require('multer');
const Employee = require('../models/Employee');
const router = express.Router();
// Multer Setup
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});
const upload = multer({ storage: storage });
// 1. CREATE
router.post('/', upload.single('image'), async (req, res) => {
try {
const { name, email, phone, designation, experience, salary } = req.body;
if (!req.file) return res.status(400).json({ message: 'Image is required' });
const newEmployee = new Employee({
name, email, phone, designation, experience, salary,
image: req.file.filename
});
await newEmployee.save();
res.status(201).json({ message: 'Employee added!', employee: newEmployee });
} catch (error) {
res.status(500).json({ message: 'Error saving employee', error: error.message });
}
});
// 2. READ
router.get('/', async (req, res) => {
try {
const employees = await Employee.find();
res.status(200).json(employees);
} catch (error) {
res.status(500).json({ message: 'Error fetching data', error: error.message });
}
});
// 3. UPDATE
router.put('/:id', upload.single('image'), async (req, res) => {
try {
const { name, email, phone, designation, experience, salary } = req.body;
let updateData = { name, email, phone, designation, experience, salary };
if (req.file) {
updateData.image = req.file.filename;
}
const updatedEmployee = await Employee.findByIdAndUpdate(req.params.id, updateData, { new: true });
res.status(200).json({ message: 'Employee updated!', employee: updatedEmployee });
} catch (error) {
res.status(500).json({ message: 'Error updating employee', error: error.message });
}
});
// 4. DELETE
router.delete('/:id', async (req, res) => {
try {
await Employee.findByIdAndDelete(req.params.id);
res.status(200).json({ message: 'Employee deleted!' });
} catch (error) {
res.status(500).json({ message: 'Error deleting employee', error: error.message });
}
});
module.exports = router;
🎨 Part 2: Frontend Code (Inside frontend/src folder)
1. frontend/src/index.css
body {
background-color: #ffffff;
color: #222222;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
}
input {
background-color: #ffffff;
color: #333333;
border: 1px solid #cccccc;
border-radius: 4px;
padding: 8px 12px;
margin-bottom: 5px;
width: 100%;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #0000ff;
}
button {
cursor: pointer;
}
2. frontend/src/App.jsx
import EmployeeManager from './components/EmployeeManager';
function App() {
return (
<div>
<center>
<h1 style={ { color: '#333', marginTop: '20px', fontFamily: 'sans-serif' } }>
MERN Employee Dashboard 🚀
</h1>
</center>
<EmployeeManager />
</div>
);
}
export default App;
3. frontend/src/components/EmployeeManager.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';
import '../index.css';
const EmployeeManager = () => {
const [employees, setEmployees] = useState([]);
const [formData, setFormData] = useState({
name: '', email: '', phone: '', designation: '', experience: '', salary: ''
});
const [image, setImage] = useState(null);
const [editId, setEditId] = useState(null);
const fetchEmployees = async () => {
try {
const response = await axios.get('http://localhost:5000/api/employees');
setEmployees(response.data);
} catch (error) {
console.error("Error fetching data:", error);
}
};
useEffect(() => {
fetchEmployees();
}, []);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleFileChange = (e) => {
setImage(e.target.files[0]);
};
const handleSubmit = async (e) => {
e.preventDefault();
const submitData = new FormData();
for (let key in formData) {
submitData.append(key, formData[key]);
}
if (image) submitData.append('image', image);
try {
if (editId) {
await axios.put(`http://localhost:5000/api/employees/${editId}`, submitData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
alert('Employee Updated Successfully! ✏️');
} else {
await axios.post('http://localhost:5000/api/employees', submitData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
alert('Employee Added Successfully! 🎉');
}
setFormData({ name: '', email: '', phone: '', designation: '', experience: '', salary: '' });
setImage(null);
setEditId(null);
fetchEmployees();
} catch (error) {
alert('Failed to save data ❌');
}
};
const handleDelete = async (id) => {
if (window.confirm("Are you sure you want to delete this employee? 🗑️")) {
try {
await axios.delete(`http://localhost:5000/api/employees/${id}`);
fetchEmployees();
} catch (error) {
alert("Delete failed!");
}
}
};
const handleEditClick = (emp) => {
setFormData({
name: emp.name, email: emp.email, phone: emp.phone,
designation: emp.designation, experience: emp.experience, salary: emp.salary
});
setEditId(emp._id);
};
return (
<div style={ { padding: '20px', maxWidth: '800px', margin: 'auto', fontFamily: 'sans-serif' } }>
{/* FORM */}
<h2>{editId ? "Edit Employee" : "Add New Employee"}</h2>
<form onSubmit={handleSubmit} style={ { display: 'grid', gap: '10px', marginBottom: '30px' } }>
<input type="text" name="name" placeholder="Name" value={formData.name} onChange={handleChange} required />
<input type="email" name="email" placeholder="Email" value={formData.email} onChange={handleChange} required />
<input type="number" name="phone" placeholder="Phone" value={formData.phone} onChange={handleChange} required />
<input type="text" name="designation" placeholder="Designation" value={formData.designation} onChange={handleChange} required />
<input type="number" name="experience" placeholder="Experience (Years)" value={formData.experience} onChange={handleChange} required />
<input type="number" name="salary" placeholder="Salary" value={formData.salary} onChange={handleChange} required />
<input type="file" accept="image/*" onChange={handleFileChange} required={!editId} />
<button type="submit" style={ { padding: '10px', background: editId ? 'orange' : 'blue', color: 'white', border: 'none' } }>
{editId ? "Update Employee" : "Save Employee"}
</button>
{editId && <button type="button" onClick={() => { setEditId(null); setFormData({name: '', email: '', phone: '', designation: '', experience: '', salary: ''}) }}>Cancel Edit</button>}
</form>
<hr />
{/* TABLE */}
<h2>Employee List</h2>
<table border="1" width="100%" cellPadding="10" style={ { textAlign: 'left', borderCollapse: 'collapse' } }>
<thead>
<tr style={ { background: '#333', color: 'white' } }>
<th>Image</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Designation</th>
<th>Exp.</th>
<th>Salary</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{employees.length > 0 ? (
employees.map((emp) => (
<tr key={emp._id}>
<td>
<img src={`http://localhost:5000/uploads/${emp.image}`} alt="Profile" style={ { width: '50px', height: '50px', borderRadius: '50%' } } />
</td>
<td>{emp.name}</td>
<td>{emp.email}</td>
<td>{emp.phone}</td>
<td>{emp.designation}</td>
<td>{emp.experience} Yrs</td>
<td>₹{emp.salary}</td>
<td>
<button onClick={() => handleEditClick(emp)} style={ { marginRight: '10px', padding: '5px 10px', background: 'green', color: 'white', border: 'none' } }>Edit ✏️</button>
<button onClick={() => handleDelete(emp._id)} style={ { padding: '5px 10px', background: 'red', color: 'white', border: 'none' } }>Delete 🗑️</button>
</td>
</tr>
))
) : (
<tr><td colSpan="8" style={ { textAlign: 'center' } }>No Employees Found</td></tr>
)}
</tbody>
</table>
</div>
);
};
export default EmployeeManager;
🚀 Key Points to Remember
-
Multer Configuration - Images are stored in the
uploads/folder - CORS Setup - Backend allows frontend requests from any origin
-
Image Access - Frontend accesses images via
http://localhost:5000/uploads/filename - Edit Functionality - Image update is optional during edit
- Validation - Image is required during creation but optional during update
Happy coding! 🎉
Troubleshooting Tips:
- Make sure MongoDB is running locally
- Create the
uploadsfolder manually in the backend directory - Install all dependencies:
npm install express mongoose cors multer dotenv - Use
npm installfor frontend dependencies too

Top comments (0)