Uploading Images – Express and React

In this article I’m going to show you how to upload an image from a react app to an express server using a library called multer. This will allow us to store the image in the server’s file system so they can be uploaded and downloaded by any client.

React

Let’s start with a basic react app. This app just contains a single form that allows the user to select an image (file), provide a description (string), and submit them.

import { useState } from 'react'

export default function App() {
  const [file, setFile] = useState()
  const [description, setDescription] = useState("")

  const submit = async event => {
    event.preventDefault()

    // Send the file and description to the server
  }

  return (
    <div className="App">
      <form onSubmit={submit}>
        <input
          filename={file} 
          onChange={e => setFile(e.target.files[0])} 
          type="file" 
          accept="image/*"
        ></input>
        <input
          onChange={e => setDescription(e.target.value)} 
          type="text"
        ></input>
        <button type="submit">Submit</button>
      </form>
    </div>
  )
}

So far this looks like a basic form, the only weird thing is that the file input has a type file and when the value changes, we selecting the event.target.files[0] and storing that with useState.

Every time the user selects a file, we’re storing the File object. This object contains all of the information about the selected file and we just need to send this data to the server when the form is submitted.

If we’re using axios, the submit file function will look like this:

const submit = async event => {
  event.preventDefault()

  const formData = new FormData()
  formData.append("image", file)
  formData.append("description", description)

  const result = await axios.post('/images', formData, { headers: {'Content-Type': 'multipart/form-data'}})
  console.log(result.data)
}

If you’re used to just posting JSON data to a server, this can look a little bit weird. An image is not a valid JSON data type which means we would either have to try and convert it to a string, send it as a json object, then convert it back to image data on the server. Or we can send it to the server using this FormData object, which is a much better option.

When we create a new FormData object, we just append all of the data that we want to send to this object. There’s the image file, and then anything else you want to send, in this case a description.

Then we send that data to the server, but we have to specify that the content type is 'multipart/form-data' so the server knows what kind of data we might be sending.

So that’s it for the client side, what about the server? What does the server need to do when we make a post request to '/images' with this form data?

Express

Let’s start with a basic express server:

const express = require('express')

const app = express()

app.post('/images', (req, res) => {

  res.send("🤗")
})

app.listen(8080, () => console.log("listening on port 8080"))

So a basic express server that is listening for a post request to /images. Obviously this isn’t going to do anything with the image data, but at least we get a nice response 🤗.

The question now is, how do we handle the image data coming into this request and store the image as a file on the server?

Upload Image

Well luckily for us, there is a fantastic library called multer that will handle all of that for us.

First install the library:

npm i multer

Then use the library:

// 1
const multer = require('multer')
// 2
const upload = multer({ dest: 'images/' })

// 3
app.post('/images', upload.single('image'), (req, res) => {
  // 4
  const imagePath = req.file.path
  const description = req.body.description

  // Save this data to a database probably

  console.log(description, imagePath)
  res.send({description, imagePath})
})
  1. Require the multer library.
  2. Specify the folder that the files should be saved to. We’re just going to save it to a folder called images on the server.
  3. Add a middleware function that will accept the image data from the client side form data as long as it’s called image. Which is exactly what we called it in the react app formData.append("image", file)
  4. Once multer has successfully saved the image, we can access the image data from req.file and any other form data from req.body.

Multer creates a unique name for the file, so the path will be something like images/d54c8136cd238804b67e4a0c56427f8b

We probably want to save this data to a database so we can query it back later, but we don’t care about that right now. This is just a post about uploading images.

We’re going to send the description and the path to the image back to the client so we can use it to view the uploaded image.

Download Image

The image is currently being stored in a directory called images on the server. That’s great, but there’s currently no way of getting the image back from the server. We want to allow the client to request the image in an img tag like this:

<img src="images/d54c8136cd238804b67e4a0c56427f8b">

Then the server needs to send the image to the client. This can be done in many ways, but the easiest way is to just let express serve these as static files:

app.use('/images', express.static('images'))

This allows anyone to view any image in the images directory. This might be exactly what you want, then yay, you’re done. But you might want to keep images private and only allow authorized users to view images. In that case, you’ll want to do something more like this:

const fs = require('fs')

app.get('/images/:imageName', (req, res) => {
  // do a bunch of if statements to make sure the user is 
  // authorized to view this image, then

  const imageName = req.params.imageName
  const readStream = fs.createReadStream(`images/${imageName}`)
  readStream.pipe(res)
})

Final Code

Here’s what the final code might look like for this app. The react app posts and image and then shows that image in an img tag. The server stores the image in its file system and serves the images when a request for an image is made:

React

import { useState } from 'react'
import axios from 'axios'

export default function App() {
  const [file, setFile] = useState()
  const [description, setDescription] = useState("")
  const [image, setImage] = useState()

  const submit = async event => {
    event.preventDefault()

    const formData = new FormData()
    formData.append("image", file)
    formData.append("description", description)

    const result = await axios.post('/images', formData, { headers: {'Content-Type': 'multipart/form-data'}})
    setImage(result.data.imagePath)
  }

  return (
    <div className="App">
      <form onSubmit={submit}>
        <input
          filename={file} 
          onChange={e => setFile(e.target.files[0])} 
          type="file" 
          accept="image/*"
        ></input>
        <input
          onChange={e => setDescription(e.target.value)} 
          type="text"
        ></input>
        <button type="submit">Submit</button>
      </form>
      { imagePath && <img src={imagePath}>}
    </div>
  )
}

Express

const express = require('express')
const fs = require('fs')
const multer = require('multer')

const upload = multer({ dest: 'images/' })

const app = express()

// app.use('/images', express.static('images'))
app.get('/images/:imageName', (req, res) => {
  // do a bunch of if statements to make sure the user is 
  // authorized to view this image, then

  const imageName = req.params.imageName
  const readStream = fs.createReadStream(`images/${imageName}`)
  readStream.pipe(res)
})

app.post('/images', upload.single('image'), (req, res) => {
  const imagePath = req.file.path
  const description = req.body.description

  // Save this data to a database probably

  console.log(description, imagePath)
  res.send({description, imagePath})
})

app.listen(8080, () => console.log("listening on port 8080"))

Leave a comment