React Node JS File Upload - Do It Right From the Start
React Node JS File Upload - Do It Right From the Start

In this post we will look on React and NodeJS file upload and how to bind them properly.

Really often we need to upload some files from our React application and we want to upload them to our server which is typically build with NodeJS because we want to store these files on the backend. The question is how to do that efficiently.

Initial project

Here I already prepared for us 2 folders: backend and frontend.

// backend/src/index.JS
const express = require('express')
const cors = require('cors')

const app = express()
app.use(express.json())
app.use(express.urlencoded({extended: true}))
app.use(cors())

app.listen(3000, () => {
    console.log('Server is running')
})

Here is our server with plain Express application without any logic. We only added here a cors package so we can make requests from React project to our API.

Additionally we have a frontend application which is a React application generated with Vite.

const App = () => {
  return (
    <div>
      <form>
        <input type="file" multiple />
        <button type="submit">Upload files</button>
      </form>
    </div>
  )
}

Here we have just an input to upload multiple files and a button to submit them.

initial project

Here is how it looks in browser.

Frontend implementation

First let's apply changes to our client side. We want to store files that we upload inside the state so we can upload them later.

const App = () => {
  const [files, setFiles] = useState([]);
  const changeFiles = (e) => {
    setFiles(e.target.files);
  };
  return (
    <div>
      <form>
        <input type="file" multiple onChange={changeFiles} />
        <button type="submit">Upload files</button>
      </form>
    </div>
  )
}

Here we created a state files for all items that we upload and we can use them later.

const App = () => {
  const [files, setFiles] = useState([]);
  const changeFiles = (e) => {
    setFiles(e.target.files);
  };
  const uploadFiles = (e) => {
    e.preventDefault();
    console.log(files)
  };
  return (
    <div>
      <form onSubmit={uploadFiles}>
        <input type="file" multiple onChange={changeFiles} />
        <button type="submit">Upload files</button>
      </form>
    </div>
  )
}

We added an onSubmit on our form and logged our list of files for upload.

log files

As you can see we can now access a list of prepared files.

Preparing files

The next question is how we will implement files uploading on the backend. The most popular package to upload files in NodeJS is called Multer.

Multer is a NodeJS middleware to handle multipart/form-data, which is primary used for uploading files.

But most importantly in order for it to work we must send all our files in the specific format as a formData.

const uploadFiles = (e) => {
  e.preventDefault();
  const formData = new FormData();
  for (const file of files) {
    formData.append("photos", file);
  }
  axios
    .post("http://localhost:3000/files", formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
    })
    .then((response) => {
      console.log("response", response.data);
    });
};

Here we created a formData property and appened there a list of files in photos property. It is important that we append all our data in a single key as exactly this photos key is what we will read on the backend.

After this we are doing an API call to our backend with FormData. Most importantly we set our headers to multipart/form-data so backend understands what we send.

404

As you can see after we submit our files our request flies to API but obviously we didn't implement it yet.

Configuring Multer

Now we must add Multer configuration to be able to access our files.

const multer = require("multer");
const upload = multer({ dest: 'uploads/' });
...
app.post("/files", upload.array("photos"), (req, res) => {
  res.json({ files: req.files });
});

Here we added multer, created an upload property which stores the destination of our files and added upload.array middleware to our request. Most important part is that we use the same key photos like in the client.

good request

As you can see our request was successful, our files were saved and we got a response with all urls back on the client. We get here a destination, encoding, filename, mimetype, size and others.

bad ext

Our files were uploaded but they don't have any extension but just unique names. It is not the way how I would like to have them.

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, process.cwd() + "/uploads");
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname));
  },
});
const upload = multer({ storage });

In order to tune that we must do storage ourselves by calling diskStorage. Inside we provide a destination and a filename. Our destination is /uploads but for filename we merge a date with the extension of the original name.

Now if we try to upload files it will create them with unique names and the correct extension.

Accessing files

The next thing that we need is to allow our frontend to access our files from the backend. As of now uploads folder is not binded to any URL which our client can access.

app.use(express.static(process.cwd() + "/uploads"));

This is enough to serve our uploads folder as static

static

As you can see now we can access our files directly in the root.

Rendering images

The last thing that I want to do inside our client is to render images after we got them back from the backend. It makes a lot of sense to show user what we uploaded on the screen.

const App = () => {
  ...
  const [images, setImages] = useState([]);
  const uploadFiles = (e) => {
    ...
    axios
      .post("http://localhost:3000/files", formData, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      })
      .then((response) => {
        console.log("response", response.data);
        const filenames = response.data.files.map((file) => file.filename);
        setImages(filenames);
      });
  };
  return (
    <div>
      ...
      <div>
        {images.map((image) => (
          <img key={image} src={`http://localhost:3000/${image}`} />
        ))}
      </div>
    </div>
  );
};

Here we created a new state images to store filenames that we want to render. We collect names when our files are uploaded and store them in state to render on the screen.

uploaded

As you can see we successfully uploaded files to the server and rendered them on the screen.

And actually if you want to improve your React knowledge and prepare for the interview I highly recommend you to check my course React Interview Questions.

📚 Source code of what we've done