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 theclient
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.