MongoDB Image Storage: Binary Encoding Guide

MongoDB Image Storage: Binary Encoding Guide

ยท

6 min read

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.

ย