Build a MERN Stack File Upload App with Progress Bar and Metadata Storage

Learn how to create a MERN stack file upload app with real-time progress tracking and metadata storage in MongoDB.

NodeJS

ExpressJS

ReactJS

MongoDB

Build a MERN Stack File Upload App with Progress Bar and Metadata Storage

Creating a file upload app with the MERN stack (MongoDB, Express, React, and Node.js) is a powerful way to learn full-stack development while implementing practical file handling features. In this article, we’ll go step-by-step through setting up a file upload app that allows users to upload files and shows upload progress on the UI.

1. Project Setup

First, we’ll set up our MERN stack project structure.

Folder Structure:

mern-file-uploader/
├── client/      # React frontend
└── lib/
└── model/
└── index.js

Install Dependencies

Navigate to the root directory and initialize the client and root folders:

mkdir mern-file-uploader
cd mern-file-uploader

# Initialize nodejs app and install dependencies
npm init -y
npm install express mongoose multer cors

# Create React app with ViteJS
npm create vite@latest

Video Tutorial if you don't like to read complete blog

2. Backend with Node.js and Express

Update package.json in root folder for type as module to support latest ESM module syntax.

{
  "name": "express-upload",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "dev": "npx nodemon index.js"
  },
  "keywords": [],
  "author": "Ghazi Khan",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.21.1",
    "mongoose": "^8.7.2",
    "multer": "1.4.5-lts.1"
  }
}

Create a file named index.js inside the root directory to set up the backend.

Setting Up Express Server

// index.js
import express from "express";
import multer from "multer";
import cors from "cors";
import path from "path";
import { fileURLToPath } from "url";

import { connectDB } from "./lib/db.js";
import UploadModel from "./model/upload.schema.js";

const app = express();

const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
const __dirname = path.dirname(__filename); // get the name of the directory

// Middleware
app.use(cors());
app.use("/uploads", express.static(path.join(__dirname, "uploads")));

// Multer Config
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "uploads");
  },
  filename: (req, file, cb) => {
    cb(null, file.originalname);
  },
});

const upload = multer({ storage });

// Define APIs
app.post("/upload", upload.single("file"), async (req, res) => {
  try {
    console.log(req.file);

    if (!req.file) {
      return res.status(400).json({ message: "No file uploaded!" });
    }

    const newFile = new UploadModel({
      name: req.file.originalname,
      mimetype: req.file.mimetype,
      path: req.file.path,
    });

    await newFile.save();

    res.status(201).json({ message: "File uploaded successfully!" });
  } catch (err) {
    console.log("Upload Error", err);
  }
});

app.get("/files", async (req, res) => {
  const files = await UploadModel.find().lean().exec();
  return res.status(200).send(files);
});

// Server Start
app.listen(4200, async () => {
  console.log("Server Started at: 4200");
  await connectDB();
});

3. Saving Metadata in MongoDB

Each file’s metadata is saved in MongoDB through the Mongoose model File, capturing key details like filename, path, and upload date. When a file is uploaded, its details are stored for later retrieval.


4. Frontend with React and Axios

In the client folder, create a component App.js to handle file upload and show progress.

Installing Axios

Inside the client directory:

npm install axios react-dropzone

Creating the Upload Component

import { Button } from "@/components/ui/button.jsx";
import { Progress } from "@/components/ui/progress.jsx";
import { BASE_URL, useUpload } from "@/useUpload.js";

function App() {
  const {
    getRootProps,
    getInputProps,
    isDragActive,
    file,
    uploadPercentage,
    onUpload,
    allFiles,
  } = useUpload();

  return (
    <div className="h-screen w-full p-10 gap-10 flex flex-col items-center justify-center">
      <div
        className="border bg-gray-100 shadow p-3 h-32 w-1/2 rounded-md flex items-center justify-center "
        {...getRootProps()}
      >
        <input {...getInputProps()} />
        {isDragActive ? (
          <p>Drop the files here ...</p>
        ) : (
          <p>Drag 'n' drop some files here, or click to select files</p>
        )}
      </div>

      {uploadPercentage > 0 && <Progress value={uploadPercentage} />}

      <Button onClick={onUpload}>Upload</Button>

      {file && (
        <div>
          <h1>Preview</h1>

          <img
            className="h-32 w-32 object-fill"
            src={file.preview}
            alt={file.name}
          />
        </div>
      )}

      <div>
        <h1>All Uploaded Files</h1>

        {allFiles.map((file) => (
          <div key={file._id}>
            <img
              className="h-32 w-32 object-fill"
              src={BASE_URL + "/" + file.path}
              alt={file.name}
            />
            <p>{file.name}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export default App;

Custom hook for handling upload logic

// useUpload.js

import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { useDropzone } from "react-dropzone";
import { toast } from "sonner";

export const BASE_URL = "http://localhost:4200";

export const useUpload = () => {
  const [file, setFile] = useState(null);
  const [files, setFiles] = useState([]);
  const [uploadPercentage, setUploadPercentage] = useState(0);

  useEffect(() => {
    getAllFiles();
  }, []);

  const onDrop = useCallback((acceptedFiles) => {
    if (acceptedFiles.length > 0) {
      const uploadedFile = acceptedFiles[0];
      uploadedFile["preview"] = URL.createObjectURL(uploadedFile);

      setFile(uploadedFile);
    }
  }, []);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    maxFiles: 1,
    accept: { "image/*": [] },
  });

  const uploadFile = async () => {
    const formData = new FormData();
    formData.append("file", file);

    const { status, data } = await axios.post(BASE_URL + "/upload", formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
      onUploadProgress: (progressEvent) => {
        const percentCompleted = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total,
        );
        setUploadPercentage(percentCompleted);
      },
    });

    if (status === 201) {
      toast("Success!", {
        description: data.message,
      });
      await getAllFiles();
      setFile(null);
    }
  };

  const getAllFiles = async () => {
    const { data } = await axios.get(BASE_URL + "/files");
    setFiles(data);
  };

  return {
    isDragActive,
    getRootProps,
    getInputProps,
    file,
    uploadPercentage,
    allFiles: files,
    onUpload: uploadFile,
  };
};

Make sure to run both the backend and frontend:

  • Backend: Run node index.js
  • Frontend: Run npm start in the client folder.

6. Conclusion

You’ve successfully built a file upload app using the MERN stack! This app allows users to upload files, shows upload progress, and saves metadata in MongoDB. This project can be expanded further by implementing user authentication, file deletion, or even different storage options like Amazon S3.


Get latest updates

I post blogs and videos on different topics on software
development. Subscribe newsletter to get notified.


You May Also Like

Build a Loan EMI Calculator in React and Deploy it on GitHub Pages

Build a Loan EMI Calculator in React and Deploy it on GitHub Pages

Learn how to build a Loan EMI Calculator with React to compute monthly EMIs, total repayment, and interest. Deploy your app online for free using GitHub Pages.

Express.js Crash Course: Build a RESTful API with Middleware

Express.js Crash Course: Build a RESTful API with Middleware

Learn to build a RESTful API using Express.js, covering middleware, routing, and CRUD operations in just 30 minutes.

Can Next.js Replace Your Backend? Pros, Cons, and Real-World Use Cases

Can Next.js Replace Your Backend? Pros, Cons, and Real-World Use Cases

Explore whether Next.js can fully replace your backend server. Learn about the advantages, limitations, and use cases for using Next.js as a full-stack solution for modern web development projects.