Uploading files in React with Progress bar using Express server
Tags: react, node, upload, express
Published: 2021-01-02 06:12:30

You might come across many websites where a file needs to be uploaded, like uploading a profile picture while creating a profile.
If the user has a slow network or uploads a huge file, then they might need to wait for a longer period of time after clicking on the upload button.
In such cases, it is good to show feedback to the user such as a progress bar,
rather than having the user stare at the screen and wondering what is happening.

In this tutorial, we will see how can we achieve file upload in React and Express/Node backend with help of the [multer](https://www.npmjs.com/package/multer) node library.

{% youtube nYY2hus0X3k %}

# Creating the React Project

First, create a folder named `react-upload-file-progress-bar` and create 2 directories `client` and `server` inside it.
Navigate to the `client` directory and run the following command to create the client project:

```bash
npx create-react-app .
```

## Creating the upload form

We will be making use of [react-bootstrap](https://react-bootstrap.github.io/) to style the page and display the progress bar.
So let's install it inside the client project.

```bash
yarn add bootstrap react-bootstrap
```

Import the bootstrap css in `index.js`:

```jsx
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import "bootstrap/dist/css/bootstrap.min.css"

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
)
```

Now add the following code to `App.js`

```jsx
import { Container, Row, Col, Form, Button } from "react-bootstrap"

function App() {
  return (
    <Container>
      <Row>
        <Col lg={{ span: 4, offset: 3 }}>
          <Form
            action="http://localhost:8081/upload_file"
            method="post"
            enctype="multipart/form-data"
          >
            <Form.Group>
              <Form.File
                id="exampleFormControlFile1"
                label="Select a File"
                name="file"
              />
            </Form.Group>
            <Form.Group>
              <Button variant="info" type="submit">
                Upload
              </Button>
            </Form.Group>
          </Form>
        </Col>
      </Row>
    </Container>
  )
}

export default App
```

In the above code, we have created a form with file input and an upload button.
We have styled the form using [bootstrap components](https://react-bootstrap.github.io/components/forms/).

Now if you start the application and open [http://localhost:3000](http://localhost:3000) in your browser, you would see a page as shown below:
![Form](https://dev-to-uploads.s3.amazonaws.com/i/n3om4oakow7quxtt1cm6.png)

## Binding the form with backend API

We will be making use of Axios to make API calls (upload file in our case). So let's go ahead and install it:

```bash
yarn add axios
```

Inside the `src` directory, create a subfolder named `utils` and create a file named `axios.js` with the following contents:

```js
import axios from "axios"
const axiosInstance = axios.create({
  baseURL: "http://localhost:8081/",
})
export default axiosInstance
```

This creates an instance of Axios and this instance can be reused wherever required and
it helps in avoiding the need to mention the base URL everywhere.

> localhost:8081 is the endpoint where we will be building the Node/Express server later in this tutorial.

Now let's write a handler to upload the file when the form is submitted:

```js
const [selectedFiles, setSelectedFiles] = useState()
const [progress, setProgress] = useState()

const submitHandler = e => {
  e.preventDefault() //prevent the form from submitting
  let formData = new FormData()

  formData.append("file", selectedFiles[0])
  axiosInstance.post("/upload_file", formData, {
    headers: {
      "Content-Type": "multipart/form-data",
    },
    onUploadProgress: data => {
      //Set the progress value to show the progress bar
      setProgress(Math.round((100 * data.loaded) / data.total))
    },
  })
}
```

Here we are making use of 2 local states, one to hold the uploaded file details and another to hold the upload progress percentage.
Also, make sure that you are adding the content-type header as `multipart/form-data`, so that it works similar to normal form submit
and multer will be able to parse the file in the back end.

Axios also accepts optional `onUploadProgress` property, which is a callback with details about how much data is uploaded.

Now let's bind the submit handler and the input field:

```jsx
import { useState } from "react"
import { Container, Row, Col, Form, Button, ProgressBar } from "react-bootstrap"
import axiosInstance from "./utils/axios"

function App() {
  const [selectedFiles, setSelectedFiles] = useState([])
  const [progress, setProgress] = useState()

  const submitHandler = e => {
    e.preventDefault() //prevent the form from submitting
    let formData = new FormData()

    formData.append("file", selectedFiles[0])
    axiosInstance.post("/upload_file", formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
      onUploadProgress: data => {
        //Set the progress value to show the progress bar
        setProgress(Math.round((100 * data.loaded) / data.total))
      },
    })
  }
  return (
    <Container>
      <Row>
        <Col lg={{ span: 4, offset: 3 }}>
          <Form
            action="http://localhost:8081/upload_file"
            method="post"
            encType="multipart/form-data"
            onSubmit={submitHandler}
          >
            <Form.Group>
              <Form.File
                id="exampleFormControlFile1"
                label="Select a File"
                name="file"
                onChange={e => {
                  setSelectedFiles(e.target.files)
                }}
              />
            </Form.Group>
            <Form.Group>
              <Button variant="info" type="submit">
                Upload
              </Button>
            </Form.Group>
            {progress && <ProgressBar now={progress} label={`${progress}%`} />}
          </Form>
        </Col>
      </Row>
    </Container>
  )
}

export default App
```

Also, we are showing the progress bar whenever it has some value using the ProgressBar component from react-bootstrap.

# Creating the backend Node Project

Now we have the client-side ready, let's build the server-side. Inside the `server` folder run the following command to create a node project.

```bash
    npm init -y
```

Update the package.json that is created with the following start script:

```json
{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
```

Now we need to have the following modules added to our project:

- [express](https://expressjs.com/) - Used to create a web framework with node.js
- [multer](https://www.npmjs.com/package/multer) - A node.js middleware for handling `multipart/form-data`,
  which is primarily used for uploading files
- [cors](https://www.npmjs.com/package/cors) - Enabling CORS policies for the client URL.

Run the following command to install the above packages in the `server` project:

```bash
yarn add express multer cors
```

Now create a file named `upload.js` inside the `server` project with the following code:

```js
const multer = require("multer")
const storage = multer.diskStorage({
  //Specify the destination directory where the file needs to be saved
  destination: function (req, file, cb) {
    cb(null, "./uploads")
  },
  //Specify the name of the file. The date is prefixed to avoid overwriting of files.
  filename: function (req, file, cb) {
    cb(null, Date.now() + "_" + file.originalname)
  },
})

const upload = multer({
  storage: storage,
})

module.exports = upload
```

Here we are creating the multer instance, by specifying the destination and the file name in which the uploaded file needs to be saved.

Now create a file named `index.js` with the following code:

```js
const express = require("express")
const upload = require("./upload")
const multer = require("multer")
const cors = require("cors")

const app = express()

//Add the client URL to the CORS policy
const whitelist = ["http://localhost:3000"]
const corsOptions = {
  origin: function (origin, callback) {
    if (!origin || whitelist.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error("Not allowed by CORS"))
    }
  },
  credentials: true,
}
app.use(cors(corsOptions))

app.post("/upload_file", upload.single("file"), function (req, res) {
  if (!req.file) {
    //If the file is not uploaded, then throw custom error with message: FILE_MISSING
    throw Error("FILE_MISSING")
  } else {
    //If the file is uploaded, then send a success response.
    res.send({ status: "success" })
  }
})

//Express Error Handling
app.use(function (err, req, res, next) {
  // Check if the error is thrown from multer
  if (err instanceof multer.MulterError) {
    res.statusCode = 400
    res.send({ code: err.code })
  } else if (err) {
    // If it is not multer error then check if it is our custom error for FILE_MISSING
    if (err.message === "FILE_MISSING") {
      res.statusCode = 400
      res.send({ code: "FILE_MISSING" })
    } else {
      //For any other errors set code as GENERIC_ERROR
      res.statusCode = 500
      res.send({ code: "GENERIC_ERROR" })
    }
  }
})

//Start the server in port 8081
const server = app.listen(8081, function () {
  const port = server.address().port

  console.log("App started at http://localhost:%s", port)
})
```

In the above code,

- We have created a POST route at `/upload_file` and call upload function exported from `upload.js`.
  The name `file` passed inside the `upload.single()` function should match with that of `FormData` in the axios call written before.
- We have added the CORS policy for out client URL. This code snippet can be reused in any express project which requires to handle CORS.
- Multer will add the details of the file uploaded to `req.file`. So if `req.file` does not have any data, that means the file is not uploaded.
  Multer by default does not throw any error if the file is missing. So we are throwing an express error with a message `FILE_MISSING`
- We have an error handler for express which looks for both [Multer errors](https://www.npmjs.com/package/multer#error-handling)
  and [express errors](https://expressjs.com/en/guide/error-handling.html) and we pass the appropriate error code in the response.

Before running the application, let's create the directory `uploads` where the uploaded files will be saved.

Now if you run the application, using the command `npm start` in 2 separate terminals,
one inside the `client` and another inside the `server` directory, you will see the progress bar in action:

![Upload Animation](https://dev-to-uploads.s3.amazonaws.com/i/rsq6hkqx4l9am0imueua.gif)

> I have used a huge file (200MB) to upload, since uploading to localhost is pretty fast and we will not be able to see the progress bar correctly.
> You can make use of the network throttling feature of the browser as well.

If you check the uploads directory now, you should be able to see the file there:
![Uploaded File](https://dev-to-uploads.s3.amazonaws.com/i/4b84ud9xw7xm4ux9mk61.PNG)

# Error handling

Now let's show appropriate error messages when the upload has failed.

## When the file is not uploaded

If the user has failed to select a file before clicking upload, we need to inform the user.
For that, let's update `App.js` with a catch chain for the axios call:

```jsx
import { useState } from "react"
import {
  Container,
  Row,
  Col,
  Form,
  Button,
  ProgressBar,
  Alert,
} from "react-bootstrap"
import axiosInstance from "./utils/axios"

function App() {
  const [selectedFiles, setSelectedFiles] = useState([])
  const [progress, setProgress] = useState()
  const [error, setError] = useState()

  const submitHandler = e => {
    e.preventDefault() //prevent the form from submitting
    let formData = new FormData()

    formData.append("file", selectedFiles[0])
    //Clear the error message
    setError("")
    axiosInstance
      .post("/upload_file", formData, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
        onUploadProgress: data => {
          //Set the progress value to show the progress bar
          setProgress(Math.round((100 * data.loaded) / data.total))
        },
      })
      .catch(error => {
        const { code } = error?.response?.data
        switch (code) {
          case "FILE_MISSING":
            setError("Please select a file before uploading!")
            break
          default:
            setError("Sorry! Something went wrong. Please try again later")
            break
        }
      })
  }
  return (
    <Container>
      <Row>
        <Col lg={{ span: 4, offset: 3 }}>
          <Form
            action="http://localhost:8081/upload_file"
            method="post"
            encType="multipart/form-data"
            onSubmit={submitHandler}
          >
            <Form.Group>
              <Form.File
                id="exampleFormControlFile1"
                label="Select a File"
                name="file"
                onChange={e => {
                  setSelectedFiles(e.target.files)
                }}
              />
            </Form.Group>
            <Form.Group>
              <Button variant="info" type="submit">
                Upload
              </Button>
            </Form.Group>
            {error && <Alert variant="danger">{error}</Alert>}
            {!error && progress && (
              <ProgressBar now={progress} label={`${progress}%`} />
            )}
          </Form>
        </Col>
      </Row>
    </Container>
  )
}

export default App
```

In the above code, whenever an error occurs we are setting the error message to the `error` state and displaying using the
[Alert component](https://react-bootstrap.github.io/components/alerts)

![File Missing Error](https://dev-to-uploads.s3.amazonaws.com/i/xskpz50s7t29i9pe8wzk.PNG)

## Preventing huge file uploads

When we need to restrict the size of the file uploaded, we can add that configuration in `upload.js` in the `server` project:

```js
const multer = require("multer")
const storage = multer.diskStorage({
  //Specify the destination directory where the file needs to be saved
  destination: function (req, file, cb) {
    cb(null, "./uploads")
  },
  //Specify the name of the file. The date is prefixed to avoid overwriting of files.
  filename: function (req, file, cb) {
    cb(null, Date.now() + "_" + file.originalname)
  },
})

const upload = multer({
  storage: storage,
  limits: {
    fileSize: 1024 * 1024,
  },
})

module.exports = upload
```

Now let's update our switch case in `App.js` in client side:

```js
switch (code) {
  case "FILE_MISSING":
    setError("Please select a file before uploading!")
    break
  case "LIMIT_FILE_SIZE":
    setError("File size is too large. Please upload files below 1MB!")
    break

  default:
    setError("Sorry! Something went wrong. Please try again later")
    break
}
```

Now if you try to upload a file larger than 1 MB, you should see the error message:

![Large File](https://dev-to-uploads.s3.amazonaws.com/i/cs2g505bcxiejj7woxit.PNG)

## Restricting file types

When we need to allow only certain type of files, we can add a `fileFilter` to the multer configuration as shown below:

```js
const upload = multer({
  storage: storage,
  limits: {
    fileSize: 1024 * 1024,
  },
  fileFilter: (req, file, cb) => {
    if (
      file.mimetype == "image/png" ||
      file.mimetype == "image/jpg" ||
      file.mimetype == "image/jpeg"
    ) {
      cb(null, true)
    } else {
      cb(null, false)
      return cb(new Error("INVALID_TYPE"))
    }
  },
})
```

Also, let's tweak the error handler in `index.js` to accommodate the new error code:

```js
// ...
//Express Error Handling
app.use(function (err, req, res, next) {
  // Check if the error is thrown from multer
  if (err instanceof multer.MulterError) {
    res.statusCode = 400
    res.send({ code: err.code })
  } else if (err) {
    // If it is not multer error then check if it is our custom error for FILE_MISSING & INVALID_TYPE
    if (err.message === "FILE_MISSING" || err.message === "INVALID_TYPE") {
      res.statusCode = 400
      res.send({ code: err.message })
    } else {
      //For any other errors set code as GENERIC_ERROR
      res.statusCode = 500
      res.send({ code: "GENERIC_ERROR" })
    }
  }
})

// ...
```

Finally, add a new case to the switch condition in `App.js`:

```js
switch (code) {
  case "FILE_MISSING":
    setError("Please select a file before uploading!")
    break
  case "LIMIT_FILE_SIZE":
    setError("File size is too large. Please upload files below 1MB!")
    break
  case "INVALID_TYPE":
    setError(
      "This file type is not supported! Only .png, .jpg and .jpeg files are allowed"
    )
    break

  default:
    setError("Sorry! Something went wrong. Please try again later")
    break
}
```

Now upload a file that is not an image and see if it shows the error:

![Invalid Type](https://dev-to-uploads.s3.amazonaws.com/i/m3m8joaq0oed2wz3ull4.PNG)

# Source code

You can view the complete [source code here](https://github.com/collegewap/react-upload-file-progress-bar).