How to Parse Media Files with Multer

Ifeoma Imoh

This post will describe how to use Multer — a Node.js middleware used to handle multipart/form-data, and it makes the process of uploading files easy. We will be using it in a Next.js application where we will be leveraging one of its core features called API routes which allows us to write server-side logic within our Next.js applications.

We would first create an API route and write some logic that describes how Multer can be used to parse files. We will then upload the said file(s) to Cloudinary. To spice up things further, we will build a simple UI that will allow us to interact with our API and upload some files. Let's get to it!

Here is a link to the demo CodeSandbox.

Setting up the Project

Create a Next.js app using the following command:

1npx create-next-app my-project

Next, add the project dependencies using the following command:

1npm install cloudinary multer axios

Setting up Cloudinary

To use the services Cloudinary provides, you need to first sign up for a free Cloudinary account if you don’t have one already. Displayed on your account’s Management Console (aka Dashboard) are important details: your cloud name, API key, etc.

You will need your cloud name and API key to initialize the Cloudinary SDK and API secret to authenticate requests we will be making to the Cloudinary servers. Next, let's create some environment variables to hold the Cloudinary details.

Create a file called .env at the root of the project with the content below:

1CLOUD_NAME = YOUR CLOUD NAME HERE
2API_KEY = YOUR API KEY HERE
3API_SECRET = YOUR API SECRET HERE

This will be used as the default if you set up the project on another system. To update your local environment, create a copy of the .env file by running this command:

1cp .env .env.local

By default, this local file resides in the .gitignore folder, mitigating the security risk of inadvertently exposing secret credentials to the public. Be sure to update the .env.local file with your Cloudinary credentials.

Writing the Upload Logic

Let's create a file that will hold some helper functions. At the root of your project, create a file called helper.js and add this code to it:

1const cloudinary = require("cloudinary").v2;
2cloudinary.config({
3 cloud_name: process.env.CLOUD_NAME,
4 api_key: process.env.API_KEY,
5 api_secret: process.env.API_SECRET,
6});
7export async function handleUpload(file) {
8 const res = await cloudinary.uploader.upload(file, {
9 resource_type: "auto",
10 });
11 return res;
12}

In this file, we start by bringing in the Cloudinary SDK, which we use to configure our credentials. The call to cloudinary.config() essentially authenticates requests we make to the Cloudinary servers. Next, we defined and exported a function called handleUpload that expects a file as input, uploads it to our account, and returns the response it gets.

Create the API Route

API routes are just files that reside in the pages/api directory of our Next.js application that allow us to write some server-side logic to handle HTTP requests. In our case, we will need just one route. In your pages/api directory, create a file called upload.js.

We would incrementally update the logic of the upload.js file to define our request handler.

First, we would start by bringing the necessary imports — the Multer module and our handleUpload utility function.

1import multer from "multer";
2import { handleUpload } from "../../helpers";

Next, we will set up the Multer middleware.

1const storage = multer.memoryStorage();
2const upload = multer({ storage });
3const myUploadMiddleware = upload.single("sample_file");

Multer provides us with two storage options: disk and memory storage. In the above snippet, we start by selecting the storage option we want for our Multer instance. We choose the memory storage option because we do not want to store parsed files on our server; instead, we want them temporarily stored on the RAM so that we can quickly upload them to Cloudinary.

Next, we created the Multer instance and initialized it with the storage option. Finally, since we expect only a single file when we use Multer, the call to upload.single() does just that for us; it is for uploading only a single file.

Next, let’s create a helper function that will allow us run our middleware. Add the following to your upload.js file:

1function runMiddleware(req, res, fn) {
2 return new Promise((resolve, reject) => {
3 fn(req, res, (result) => {
4 if (result instanceof Error) {
5 return reject(result);
6 }
7 return resolve(result);
8 });
9 });
10}

The runMiddleware function accepts three parameters. The first two are the request and response objects, and the third is a callback function that will be some middleware. runMiddleware returns a promise that resolves successfully when the middleware callback runs successfully and fails with an error otherwise.

Now we have the necessary moving parts. Next, we need to define and export the main handler function. Update your upload.js file like so:

1const handler = async (req, res) => {
2 try {
3 await runMiddleware(req, res, myUploadMiddleware);
4 const b64 = Buffer.from(req.file.buffer).toString("base64");
5 let dataURI = "data:" + req.file.mimetype + ";base64," + b64;
6 const cldRes = await handleUpload(dataURI);
7 res.json(cldRes);
8 } catch (error) {
9 console.log(error);
10 res.send({
11 message: error.message,
12 });
13 }
14};
15export default handler;

In the above code, we define and export the main handler function that will be triggered by any HTTP request made to /api/upload. This function starts by parsing the files contained in the request body using the Multer middleware. If it succeeds, it attaches the parsed file data to the request object.

Next, we construct a data URI from the file object that holds the base64 encoded data representing the file. This data is fed to the handleUpload function we defined earlier, which uploads the file and returns the response stored in a variable called cldRes which is then attached to the response object sent back.

We are still missing one step. It will interest you to know that all API Route handler functions, by default, provide us with middleware under the hood that automatically parses the contents of the request body, cookies, and queries. This means that by default, we don't consume the raw request body stream, and we need that because the Multer middleware is supposed to parse files contained in the raw, unadulterated request object. Thankfully, API routes also provide us with a config object that allows us to modify the settings to fit our needs.

Add the following to the end of the upload.js file:

1export const config = {
2 api: {
3 bodyParser: false,
4 },
5};

This disables body parsing and allows our handler function access to the raw stream of the request body.

Building the Frontend

We are now done with the Backend. Let's build a simple UI to interact with it. Add the following to your pages/index.js file:

1import { useState } from "react";
2import axios from "axios";
3
4export default function IndexPage() {
5 const [loading, setLoading] = useState(false);
6 const [res, setRes] = useState({});
7 const [file, setFile] = useState(null);
8 const handleSelectFile = (e) => setFile(e.target.files[0]);
9 const uploadFile = async (e) => {
10 setLoading(true);
11 e.preventDefault();
12 const data = new FormData();
13 console.log(file);
14 data.set("sample_file", file);
15 try {
16 const res = await axios.post("/api/upload", data);
17 setRes(res.data);
18 } catch (error) {
19 console.log(error);
20 } finally {
21 setLoading(false);
22 }
23 };
24 return (
25 <div className="App">
26 <label htmlFor="file" className="btn-grey">
27 {" "}
28 select file
29 </label>
30 <input
31 id="file"
32 type="file"
33 onChange={handleSelectFile}
34 multiple={false}
35 />
36 {file && <p className="file_name">{file.name}</p>}
37 <code>
38 {Object.keys(res).map(
39 (key) =>
40 key && (
41 <p className="output-item" key={key}>
42 <span>{key}:</span>
43 <span>
44 {typeof res[key] === "object" ? "object" : res[key]}
45 </span>
46 </p>
47 )
48 )}
49 </code>
50 {file && (
51 <>
52 <button className="btn-green" onClick={uploadFile}>
53 {loading ? "uploading..." : "upload to Cloudinary"}
54 </button>
55 </>
56 )}
57 </div>
58 );
59}

This component is relatively straightforward; it simply allows the user to select a file from their computer and provides them with a button to upload the file and display the result. The centerpiece of this component is the function we call uploadFile, which does the file uploading via the following steps:

  • It starts by using the FormData API to construct key-value pairs that represent the request payload. Since our payload is only the selected file, we define a key called sample_file with a value that points to the selected file. Notice that this key matches the one we defined when we earlier set up Multer on our API route. Multer only parses request payload that has an encoding of multipart/form-data, and by using the FormData API, this is automatically set for us in the request headers.
  • Next, it makes an HTTP request to our API route and passes the file as the payload. If the request succeeds, it stores the response in a state variable which is then iterated upon and rendered to the screen.

Start your application on http://localhost:3000/ with the following command:

1npm run dev

Once the app is up and running, you should be able to select a file, upload it and see the response displayed.

Find the complete project here on GitHub.

Conclusion

This tutorial walks you through using additional middleware within API routes. We saw how to parse files using Multer before uploading. Your API routes may require more logic to handle other specific concerns as your application grows in sophistication and requirements.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.