DEV Community

mass status
mass status

Posted on

Build a Complete MERN Stack CRUD App with Image Upload (Step-by-Step)

Build a Complete MERN Stack CRUD App with Image Upload (Step-by-Step)

MERN Stack CRUD Application

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
Enter fullscreen mode Exit fullscreen mode

⚙️ Part 1: Backend Code

1. backend/.env

PORT=5000
MONGO_URI=mongodb://127.0.0.1:27017/mock-crud
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

🎨 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

🚀 Key Points to Remember

  1. Multer Configuration - Images are stored in the uploads/ folder
  2. CORS Setup - Backend allows frontend requests from any origin
  3. Image Access - Frontend accesses images via http://localhost:5000/uploads/filename
  4. Edit Functionality - Image update is optional during edit
  5. Validation - Image is required during creation but optional during update

Happy coding! 🎉


Troubleshooting Tips:

  • Make sure MongoDB is running locally
  • Create the uploads folder manually in the backend directory
  • Install all dependencies: npm install express mongoose cors multer dotenv
  • Use npm install for frontend dependencies too

Top comments (0)