Images play a crucial role in today's internet landscape, especially within full-stack applications where users often need to upload various media. When it comes to storing images in MongoDB, developers have several methods to consider, particularly when using Mongoose.
One approach is to store the image directly on the backend server and save its path in the database collection.
Alternatively, developers can encode the image to base64 or binary format before storing it in the database.
The recommended practice is to utilize a dedicated storage server like Amazon S3 or Google Cloud Storage.
But what's the most efficient method?
Storing images on the backend server might lead to memory issues, especially during high loads, and base64 encoding isn't suitable for large image sizes. On the other hand, binary encoding offers better support for larger images and aligns with industry best practices, advocating for external storage servers.
So, which option should you prefer?
For beginners venturing into full-stack development, experimenting with encoding methods is valuable for understanding how they work. However, as you progress, transitioning to a storage server approach will enhance scalability and performance, aligning with industry standards and best practices.
Where to start?
To kickstart your image upload process in MongoDB, you'll need to set up both your front-end and back-end file systems. Begin by installing the necessary dependencies, namely Multer and Mongoose, using npm:
npm i multer
npm i mongoose
Multer is a node.js middleware for handling
multipart/form-data
, which is primarily used for uploading files.Mongoose is a MongoDB object modelling tool designed to work in an asynchronous environment. Mongoose supports Node.jsand Deno(alpha).
Setting up with you Schmea
Once you've installed the dependencies, it's time to define your schema:
const mongoose = require("mongoose");
const ImageUploadSchema = new mongoose.Schema({
username: String, // Name of the user
password: String, // Email address of the user
email: String, // Password of the user
profileImage: {
data: Buffer,
contentType: String,
}, // Profile image of the user
});
// Creating ImageUploadSchema using the UserSchema
const ImageModel = mongoose.model("users", ImageUploadSchema);
// Exporting UserModel and Carmodel for use in other modules
module.exports = { ImageModel };
The profileImage
field in the schema above is crucial as it's designated for storing images in binary format.
Setting Up Backend
After finalizing your schema, proceed to establish the connection between your backend and MongoDB server using Mongoose, and set up Multer for handling file uploads.
// Importing required modules
const mongoose = require("mongoose");
const multer = require("multer");
const express = require("express");
require("dotenv").config(); // Loading environment variables from .env file
const { ImageModel } = require("../modules/MDSchema"); // Import ImageModel Schema
// Creating an instance of Express
const app = express();
const port = process.env.PUBLIC_PORT || 3000;
// Middleware for enabling CORS
app.use(cors());
// Middleware for parsing JSON bodies
app.use(express.json());
// Configuring multer storage
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
// Function to start the database connection
const startDatabase = async () => {
try {
// Connecting to MongoDB using the URI from environment variables
await mongoose.connect(process.env.MONGO_URI);
console.log("๐ฆ Connected to MongoDB");
} catch (error) {
// Handling connection error
console.error("โ Error connecting to MongoDB:", error.message);
}
};
// Starting the server if this script is the main module
if (require.main === module) {
app.listen(port, () => {
// Logging a message when server starts
console.log(`๐ Server running on PORT: ${port}`);
// Starting the database connection
startDatabase();
});
}
// Exporting the app for use in other modules
module.exports = app;
With these configurations in place, your backend will be seamlessly connected to MongoDB, ready to handle image uploads using Multer.
// Configuring multer storage
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
This section initializes Multer's virtual memory and configures storage to handle images sent from the front end.
SettinConfiguring Post Requests for File Uploads in the Backend
// Route to handle file upload
app.post("/upload", upload.single("file"), async (req, res) => {
try {
// Check if the request has an image
if (!req.file) {
res.json({
success: false,
message: "You must provide at least 1 file",
});
} else {
// Create an object for image upload
let imageUploadObject = {
username: req.body.username, // User's name
password: req.body.password, // User's email address
email: req.body.email, // User's password
file: {
data: req.file.buffer, // Image data
contentType: req.file.mimetype, // Image content type
},
};
const uploadObject = new ImageModel(imageUploadObject);
// Save the object into the database
const uploadProcess = await uploadObject.save();
// Send success response
res.json({
success: true,
message: "File uploaded successfully",
});
}
} catch (error) {
// Handle error
console.error("โ Error uploading file:", error.message);
res.status(500).send("Server Error");
}
});
In this code snippet, the upload
middleware has been pre-configured in the Multer storage session. By utilizing upload.single
, we specify that only one file will be stored, with the attribute of upload.single
corresponding to the ID of the input tag in the frontend. Incorporating this middleware grants us access to req.file
the route definition, allowing us to retrieve the received file.
We utilized req.file.buffer
and req.file.mimetype
to persist the file in the database. The buffer
holds the raw binary data of the received file, which we store in the database as-is. Additionally, req.file.mimetype
plays a crucial role as it informs the browser how to interpret the raw binary data, specifying whether it represents a PNG image, JPEG, or another format.
To explore additional information accessible from req.file
, click here. It was necessary to split the file object into two properties: data
, which stores the raw binary data, and contentType
, which contains the mimetype.
Setting up Fronend
import React, { useState } from "react";
import axios from "axios";
function Upload() {
const [formData, setFormData] = useState({
file: null,
username: "",
email: "",
password: "",
});
const [message, setMessage] = useState("");
const handleInputChange = (event) => {
setFormData({
...formData,
[event.target.name]: event.target.value,
});
};
const handleFileChange = (event) => {
setFormData({
...formData,
file: event.target.files[0],
});
};
const handleFormSubmit = async (event) => {
event.preventDefault();
try {
if (!formData.file || !formData.username || !formData.email || !formData.password) {
setMessage("Please fill out all fields");
return;
}
const formDataToSend = new FormData();
formDataToSend.append("file", formData.file);
formDataToSend.append("username", formData.username);
formDataToSend.append("email", formData.email);
formDataToSend.append("password", formData.password);
const response = await axios.post(
"{paste your backend url}",
formDataToSend,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
);
if (response.data.success) {
setMessage(response.data.message);
} else {
setMessage("File upload failed");
}
} catch (error) {
console.error("Error uploading file:", error.message);
setMessage("Server Error");
}
};
return (
<div>
<form onSubmit={handleFormSubmit}>
<input type="file" name="file" onChange={handleFileChange} />
<input
type="text"
name="username"
placeholder="Username"
value={formData.username}
onChange={handleInputChange}
/>
<input
type="email"
name="email"
placeholder="Email"
value={formData.email}
onChange={handleInputChange}
/>
<input
type="password"
name="password"
placeholder="Password"
value={formData.password}
onChange={handleInputChange}
/>
<button type="submit">Upload</button>
</form>
{message && <p>{message}</p>}
</div>
);
}
export default Upload;
How to Convert Binary Data Back to an Image?
There are primarily two approaches to achieve this. You can either convert the binary data to an image on the backend and then transmit it to the frontend, or you can directly send the binary data to the frontend and perform the conversion there. The choice between these methods largely depends on your preferences and specific use case.