Store images on MongoDB

Store images on MongoDB

Using ExpressJS, Mongoose, and Multer

Images have become a crucial part of the internet. It's not just web applications that need images, social media has made sure that users not only consume data but also produce and share them. Applications like WhatsApp, Telegram, and Discord also support sharing documents. So, as a backend developer, handling images and storing them on the database is a must. For this tutorial, I am assuming that you are fairly good with ExpressJS and can use Mongoose, or at least know how to use the MongoDB native drivers for NodeJS. I am also assuming that your Express Server is already set up with Mongoose, or that you are using the native MongoDB drivers for NodeJS

Form encoding

When making a POST request, you need to encode the data that is passed along to the backed so that it can be easily parsed. HTML forms provide three methods of encoding:

  • application/x-www-form-urlencoded: The default mode of encoding. A long string of name-values is created, where each name and value pair is separated by an =, and each pair is separated by an & so that it can be parsed by the server.
  • multipart/form-data: This encoding is used when there is a need for files to be uploaded to the server.
  • text/plain: They have been introduced as a part of the HTML 5 specification, and are not used widely in general.

Why image handling is different on Express?

When you send form data to the express backend, express is equipped with handling the application/x-www-form-urlencoded and the text/plain encodings, it cannot process the multipart/form-data encoding, which is primarily used for uploading files. This is where Multer comes in. A node.js middleware that will handle multipart encoded forms for us.

Setting up your Schema

You need to define a schema Upload.js for the collection where you are going to store your images. If you are using the native MongoDB drivers, you can skip this part.

// Upload.js
const mongoose = require("mongoose");

const UploadSchema = new mongoose.Schema({
  fileName: {
    type: String,
    required: true,
  },
  file: {
    data: Buffer,
    contentType: String,
  },
  uploadTime: {
    type: Date,
    default: Date.now,
  },
});

module.exports = Upload = mongoose.model("upload", UploadSchema);

In the above schema, the file block is the most important one, the rest can be conveniently ignored to match your requirements.

Setting up multer

Install Multer for your application:

Using npm:

npm i multer

Using yarn:

yarn add multer

Now, let's create a route that will handle file upload. But before that, let's enable our app to use multer in upload.js.

// upload.js
const express = require('express')
const multer  = require('multer')
//importing mongoose schema file
const Upload = require("../models/Upload");
const app = express()
//setting options for multer
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

This snippet makes sure that the file is parsed and stored in the memory. WARNING: Make sure that you take steps to make sure that the file being uploaded isn't huge, or else you could be looking at a Denial-of-service threat. There are some options that you can use in multer. Take a look at them here.

Using multer middleware in your route

Now that you have set up multer successfully, it is time to use it as a middleware in your requests.

app.post("/upload", upload.single("file"), async (req, res) => {
  // req.file can be used to access all file properties
  try {
    //check if the request has an image or not
    if (!req.file) {
      res.json({
        success: false,
        message: "You must provide at least 1 file"
      });
    } else {
      let imageUploadObject = {
        file: {
          data: req.file.buffer,
          contentType: req.file.mimetype
        },
        fileName: req.body.fileName
      };
      const uploadObject = new Upload(imageUploadObject);
      // saving the object into the database
      const uploadProcess = await uploadObject.save();
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

You can also use upload.array() instead of upload.single() if you are expecting to receive more than one file from the frontend. More about that here.

Code explanation

The middleware upload.single("image") is used to tell the server that only one file is being expected from the browser. The argument inside the upload.single() tells the name of the file field in the HTML form. Using this middleware enables us to use req.file inside the route definition to access the received file. We used req.file.buffer and req.file.mimetype to save the file on the database. The buffer is raw binary data of the file received and we'll store it on the database as it is. The req.file.mimetype is also very important for us as it'll tell the browser how to parse the raw binary data, i.e. what to interpret the data as, whether it be a png image or jpeg, or something else. To find out what other information can be accessed from req.file, click here. We had to break the file object into two properties, namely data, which contains the raw binary, and the contentType, which contains the mimetype.

Sending data from Frontend

Remember, multer only accepts multipart/form-data for files. That is why we need to set the encoding type to the same on our frontend.

<form action="/profile" method="post" enctype="multipart/form-data">
  <input type="file" name="avatar" />
</form>

How to convert it back to an image?

Well, there are basically two ways you can do this. You either convert the binary data to the image on the backend and then send it to the frontend, or you send the binary data to the frontend and then convert it to an image. It totally depends on your liking and your use case. How to do it? Well, that article is for another WebDev Monday's.

Did you find this article valuable?

Support Yash Aryan by becoming a sponsor. Any amount is appreciated!