One thing we miss now in the new house form, and when editing an existing house, is being able to upload images.

We are currently limited by uploading an image somewhere, and pasting the URL in the form. Not very practical for our users!

Let’s add this functionality.

Before we start, we must add an npm package, called express-fileupload:

npm install express-fileupload

and we add it as a middleware to Express, in server.js:

const fileupload = require('express-fileupload')
server.use(
  //...
  fileupload()
)

This is needed because otherwise the server can’t parse file uploads.

Next, in components/HouseForm.js, change the current input field that we used:

<p>
  <label>House picture URL</label>
  <input
    required
    onChange={event => setPicture(event.target.value)}
    type='text'
    placeholder='House picture URL'
    value={picture}
  />
</p>

to this combo of a file upload and an image visualizer:

<p>
  <label>House picture</label>
  <input
    type="file" id="fileUpload"
  />
  {picture ? <img src={picture} width="200" alt="House image" /> : ''}
</p>

If you try to reload the page, the file picker should be there! Let’s add input[type=file] to the CSS styling for forms we already have at the bottom:

<style jsx>{`
  input[type='number'],
  input[type='file'],
  select,
  textarea {
        /*... */
  }
}

Let’s also limit the file input to only accept images:

<input type="file" id="fileUpload" accept="image/*" />

See https://flaviocopes.com/how-to-accept-images-file-input/

Now we must handle the change event on this input field:

<input
  type='file'
  id='fileUpload'
  accept='image/*'
  onChange={async event => {
    const files = event.target.files
    const formData = new FormData()
    formData.append('image', files[0])

    const response = await axios.post('/api/host/image', formData)
    setPicture('http://localhost:3000' + response.data.path)
  }}
/>

This is invoked when the file input changes (an image had been selected). In there, we get the image from event.target.files and we POST it to /host/image, a new endpoint we’re going to make next.

We expect a path property coming back, which will be the URL of our image, and we assign it using the setPicture hook update function.

Let’s now make the POST /api/host/image endpoint in server.js.

We first check if the user is logged in, and we get the image from the request:

server.js

server.post('/api/host/image', (req, res) => {
  if (!req.session.passport) {
    res.writeHead(403, {
      'Content-Type': 'application/json'
    })
    res.end(
      JSON.stringify({
        status: 'error',
        message: 'Unauthorized'
      })
    )

    return
  }

  const image = req.files.image
})

Next we run npm install randomstring and we import that module:

const randomstring = require('randomstring')

we need it to generate a random string for our image, since users might submit images with the same name. I’m just going to prepend a random string to the image original name, but in the real world you might want to completely randomize it, and also check if you don’t already have that name before overwriting the file:

const fileName = randomstring.generate(7) + image.name.replace(/\s/g, '')
const path = __dirname + '/public/img/houses/' + fileName

Then we call the mv property of the uploaded image. That is provided to us by the express-fileupload module. We move it to path and then we communicate the success (or an error!) back to the client:

image.mv(path, (error) => {
  if (error) {
    console.error(error)
    res.writeHead(500, {
      'Content-Type': 'application/json'
    })
    res.end(JSON.stringify({ status: 'error', message: error }))
    return
  }

  res.writeHead(200, {
    'Content-Type': 'application/json'
  })
  res.end(JSON.stringify({ status: 'success', path: '/img/houses/' + fileName }))
})

This is the complete code for our /api/host/image endpoint:

server.post('/api/host/image', (req, res) => {
  if (!req.session.passport) {
    res.writeHead(403, {
      'Content-Type': 'application/json'
    })
    res.end(
      JSON.stringify({
        status: 'error',
        message: 'Unauthorized'
      })
    )

    return
  }

  const image = req.files.image
  const fileName = randomstring.generate(7) + image.name.replace(/\s/g, '')
  const path = __dirname + '/public/img/houses/' + fileName

  image.mv(path, error => {
    if (error) {
      console.error(error)
      res.writeHead(500, {
        'Content-Type': 'application/json'
      })
      res.end(JSON.stringify({ status: 'error', message: error }))
      return
    }

    res.writeHead(200, {
      'Content-Type': 'application/json'
    })
    res.end(
      JSON.stringify({ status: 'success', path: '/img/houses/' + fileName })
    )
  })
})

Now you should be able to successfully submit a new image for the house, and also update existing houses images!

The code for this lesson is available at https://github.com/flaviocopes/airbnb-clone-react-nextjs/tree/module-6-lesson-5


Go to the next lesson