How to upload files in NodeJS using Multer 2.0!

what uploading files to your own nodejs server feels like

Teile den Beitrag

Share on facebook
Share on linkedin
Share on twitter
Share on email

Multer is a middleware for node.js that handles multipart/form-data, and its getting a major update! The release candidate for version 2 is already up and showing some promising features such as a new Stream-based API, as well as automatic file detection. In this article, we will make a quick express backend to show the new features, and we will also create a react app to send valid form data.

Watch on Youtube

Why multer?

There are many methods to send a file via an HTTP Request. However, the most commonly used format, application/json, is not very useful for sending files, as you would have to convert them into a base64 string. Doing this increases file size drastically and adds lots of client-side and server-side processing overhead.

That is why multipart/form-data is more commonly used for uploading files in node.js. We can use FormData to send files as Blobs and we can also send some strings as parameters with it.

const data = new FormData();

Keep in mind that FormData is not a common JS Object, and instead uses it’s own methods. For instance, if you attempt to log FormData in your Web console, it will always appear to be an empty object, even if there is data inside. Also, you can’t just assign Data to it like you would with a normal JS Object. Instead, you have to use data.append("name", "string" or Blob).

Usually, in an express backend we are using BodyParser to parse the body – and BodyParser doesn’t handle multipart data. That’s why another middleware is needed. There are some options when it comes to NodeJS, and I think multer is good for the job, as it requires less setting-up with the latest v2 update.

Sending FormData from our Frontend

First, I would like to setup a quick react app to make a frontend form to send our form data from. Let’s use create-react-app to set up our frontend in the current directory.

npx create-react-app .

Next, I will remove the boilerplate content from App.js, and create a simple form which has a string parameter and a file.

import React, { useState } from "react";

import "./App.css";
import Axios from "axios";

function App() {
  const [name, setName] = useState();
  const [file, setFile] = useState();

  return (
    <div className="App">
      <header className="App-header">
        <form action="#">
          <div className="flex">
            <label htmlFor="name">Name</label>
            <input
              type="text"
              id="name"
              onChange={event => {
                const { value } = event.target;
                setName(value);
              }}
            />
          </div>
          <div className="flex">
            <label htmlFor="file">File</label>
            <input
              type="file"
              id="file"
              accept=".jpg"
              onChange={event => {
                const file = event.target.files[0];
                setFile(file);
              }}
            />
          </div>
        </form>
        <button onClick={send}>Send</button>
      </header>
    </div>
  );
}

export default App;

Also, here’s some CSS for your App.css if you want your form to look nice.

input {
  font-size: 16px;
  padding: 5px;
  margin: 10px;
}

label {
  font-size: 18px;
  width: 100px;
  text-align: left;
}

.flex {
  display: flex;
  align-items: center;
}

button {
  padding: 5px;
  width: 100px;
  border-radius: 2px;
}

As you can see, I already put our form values in the state using onChange. Note that for an input of type="file", event.target will hold a FileList. Since we’re uploading one file here, we can always put the first element in that List in our state.

In a production site, don’t forget to set the accept=".jpg" property. This will preselect the filetype for the user and filter their files according to what they can actually upload.

Now, I will use axios to make the request. To send our data, we have to append it to a FormData object as discussed earlier and attach it to our axios request.

const send = event => {
    const data = new FormData();
    data.append("name", name);
    data.append("file", file);

    Axios.post("https://httpbin.org/anything", data)
      .then(res => console.log(res))
      .catch(err => console.log(err));
  };

To test if you’re sending the data correctly, you can npm start and send it over to httpbin.org/anything. They will parse your data and send it back to you. The response should have a data object which contains a form object containing your params, and a file object containing your file blob. We will change this to send the request to our own server later on.

Sending form-data without JS

As a side note, we already covered sending FormData via JS – but sending it in a Non-JS environment is a little different.

To properly send FormData using HTML, we have to specify an enctype="multipart/form-data" in our form. The action will then send properly encoded FormData.

<form enctype="multipart/form-data" action="api/form.php" method="post">
  ...
</form>

Uploading Files with Multer v2 from our express Backend

Here comes the interesting part! Let’s bootstrap our express backend using express-generator.

npx express-generator
npm i

I’m going to change our start script in package.json to use nodemon. Also, you can install and use cors if you need. I also replace all vars with consts because I like modern code. All of this is optional, of course.

At the time of writing this article (March 9th 2020), Multer version 2 is still a release candidate, so to install it, you have to specify the version: npm i multer@2.0.0-rc.1. Once v2 is released, you can install it normally using npm i multer.

Next, let’s go into routes/index.js and make a new upload route that uses multer to parse some form data.

const multer = require("multer");

const upload = multer();
router.post("/upload", upload.single("file"), function(req, res, next) {
  ...
});

As you can see, we first make a new instance of multer, and then we call the single method on it, which takes in the name of your file in the form data (in our case “file”).

But where did all the options go?

Before, we would define options in our multer instance and the file would be uploaded by the middleware. But we didn’t define any options – so where’s the file?

The answer is in the req. Start up your server using npm start and let’s console.log("File:", req.file) to make a request from our frontend. Change your axios to request http://localhost:3000/upload and hit send.

upload files in node using multer version 2

Your server should log a file object. Notice that “path” points to your temp folder, and “stream” is now a ReadStream.

This is because now multer uploads the file to a temporary directory and gives you access to the ReadStream, rather than having you handle all the uploading while middleware is running. This allows you to handle the logic of uploading in your route itself.

Another new feature lies in the detectedMimeType and detectedFileExtension fields. This is because you do not want to trust the user to tell you the correct filetype that they’re sending. For example, anyone can make a script file and rename it to a .jpg – there you go, now you have a file that look like a picture, but it’s simply not.

Obviously, we don’t want to be XSS’d through our file upload. This is why Multer now uses Magic Bytes to ascertain the correct file type. For instance, if a user alienates the file format by renaming their file extension to .jpg, the clientReportedFileExtension will likely be ".jpg", but the detectedFileExtension will be empty.

Piping the Multer stream into our file system

Before our file is uploaded to a place where it is useful, we have to upload it there. As previously discussed, req.file now has a ReadStream, which we can pipe into a WriteStream to write to our file system.

const fs = require("fs");
const { promisify } = require("util");
const pipeline = promisify(require("stream").pipeline);

Aside from our file system we need a pipeline, which I will promisify in order to be able to await the stream. Now we turn our function into an async function and save the file to our public folder.

router.post("/upload", upload.single("file"), async function(req, res, next) {
  const {
    file,
    body: { name }
  } = req;

  const fileName = name + file.detectedFileExtension;
  await pipeline(
    file.stream,
    fs.createWriteStream(`${__dirname}/../public/images/${fileName}`)
  );

  res.send("File uploaded as " + fileName);
});

Note that I’m constructing the file name from the sent form data directly – this is something that was not easily possible before as you would not have direct access to the actual filename on disk in your route function. Now we can handle the naming of our file in the function, and append the correct file extension that was already identified by the middleware.

Sending a request should now upload your file to your public/images folder with the filename that you specified in your form, using the real detected file extension.

More fun with Multer v2

With Multer 2.0.0, it has become easier than ever to handle uploading logic in your route function.

For instance, we already discussed the new parameter detectedFileExtension. Interrupting the file upload if the user provides an unexpected format becomes easier than ever.

if (file.detectedFileExtension != ".jpg") next(new Error("Invalid file type"));

Cancelling the file upload and providing a useful error response would have been much more work using the old API.

Let’s get into some stuff that would’ve been an actual nightmare using the old API – what if, for example, I wanted to dynamically upload my file to different destinations depending on different factors?

let destination;
let fileName = req.body.name + req.file.detectedFileExtension;
if (condition1) destination = "/path/somewhere";
else if (condition2) destination = "/path/somewhere/else";
else if (condition3) {
  destination = "/completely/elsewhere";
  fileName = req.body.param2 + req.file.detectedFileExtension;
}
await pipeline(
    file.stream,
    fs.createWriteStream(`${destination}/${fileName}`)
  );

This would be an absolute horror scenario as in old multer, destination and filename were set separately in the middleware. But now, it becomes quite intuitive.

Conclusion

Multer 2.0.0 is awesome! Its faster, easier to use, the code is easier to understand and it’s more flexible. Whether or not you should start using it today is up to you, but the dev has this to say:

I can absolutely recommend you to start using v2 today, just be aware that we are changing the default limits, so I would watch this PR ? #848 so that you know that it will work for you before going from rc.1 to the final version.

– LinusU, developer of Multer

That’s why I will be using v2 for any new Projects starting now, and I’ll move my existing apps to v2 when it’s finally out.

Subscribe To Our Newsletter

Get updates and learn from the best

Explore

Möchten Sie Ihr Unternehmen pushen?

Schreiben Sie uns und bleiben Sie in Kontakt