Airbnb clone, upload the house image

Join the 2022 Full-Stack Web Dev Bootcamp!


This post is part of a new series where we build a clone of Airbnb with Next.js. See the first post here.

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/' + 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/' + 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/' + 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/' + fileName }))
  })
})

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

Awesome, we reached the end of the project!

On an application like this, we had a lot of things to build.

We also have lots of things we still miss, just to provide a basic house booking marketplace to our users.

On the top of my mind, we currently miss adding communication between host and guests, allowing to add multiple images and manage the gallery, managing superhosts, allowing different prices for different dates, blocking out days..

We’d need one year to complete all that.

Maybe in a future sequel!

Want to become a better Web Developer? Join the 2022 Web Development Bootcamp!

⭐️⭐️⭐️ Join the 2022 Web Development Bootcamp ⭐️⭐️⭐️