Multer with React and Superagent

Multer is an npm package that allows users to upload files to your designated storage location.

Summary

While the package is easy to install, the guidelines on the internet at the time of this writing (Mar 2025) have the following limitations that may be confusing for newer developers who learned only one way of doing things:

Install

Multer is an npm package, so it is installed with a terminal command:

npm install --save multer

Setup server

In the server routes file (the specific file with all the put/get commands, not the overall server file that defines the api routes), at the top:

import multer from 'multer'

const storage = multer.diskStorage({ 
  destination: (req, file, cb) => { 
    cb(null, './public/images')
  }, 
  filename: (req, file, cb) => { 
    cb(null, `${Date.now()}-${file.originalname}`)
  }, 
})

const upload = multer({ storage: storage })
Some more details about the words used above:

Other functions that can be added to this storage function are controls on file type and file size.

However, they can also be all omitted and the entire block just replaced with

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

Then, in the actual update function, we include the above function as middleware. The 'uploaded_file' is the name you give the form field in the client component. If you change this name here, make sure to use the same name in the client form.

You see we check if the file was sent, this is part of the req (similar to how body is, but is a separate component of req in Express). Express servers receive data from the client side through the req object, and so far the most common ones we've used are req.params and req.body. req.file is the one that can send whole files. There are many other req properties.

router.put('/:id', upload.single('uploaded_file'), async (req, res) => { 
let pet
try { 
  if (req.file) 
    pet = await db.updatePet(Number(req.params.id), { 
      ...req.body, 
      imgUrl: `/images/${req.file.filename}`, 
    })
    else pet = await db.updatePet(Number(req.params.id), req.body)
    
  if (!pet) res.status(404).json({ error: 'No such pet' })
  else res.status(200).json(pet)
} catch (error) { 
  if (error instanceof Error) { 
    console.error(error.message)
  } else { 
    console.error('unknown error while updating Pet')
  } res.status(500).json({ error: 'Failed to update Pet' })
 } 
})

Setup client form

This is quite tricky if you have a form with many fields and one of those fields happens to be an option to upload a file.

The reason this is tricky is because without uploading a file, a simple react form is sent to the server via the API as a JSON. But a file cannot be sent as a JSON.

So we have to change the entire form setup.

Your form will already have the normal onChange fields and the associated handleChange functions. This does not change.

The file upload bit is added by just having <input type="file"/> in the form - this will straight away give you the functionality to click and add a file from your computer.

We also need to hold this file in state. It may be easier to have it all in one function, but we decided to hold it in two different ones:

const [formState, setFormState] = useState(pet)

function handleFileChange(evt: React.ChangeEvent) { 
  if (evt.target.files) 
    setFormState({ ...formState, file: evt.target.files[0] })
} 

function handleChange(evt: React.ChangeEvent) { 
  const { name, value } = evt.target
  setFormState((prev) => ({ ...prev, [name]: value }))
}

So it's still the same form state, but the file is being handled separately, with the other values being retained with the shallow copy via the spread operator: ...prev.

So far so good.

Now the tricky part.

As I said before, we cannot send the file via JSON. So we have to convert all our form fields to FormData.

So before we had a seemingly simple mutation function call that accepted all form fields as they were:

function handleSubmit(evt: React.FormEvent) { 
  evt.preventDefault() 

  updatePetMutation.mutate({ 
    id: Number(id), 
    ownerId: ownerId, 
    name: formState.name, 
    imgUrl: formState.imgUrl, 
  })
}

And now we have to change the entire set of fields to use FormData. Then we append each item to the new formData object. However, objects of FormData accept only strings or blobs. They do not like numbers or booleans, which is why you'll see below those being cast into String().

async function handleSubmit(evt: React.FormEvent) { 
  evt.preventDefault()

  const formData = new FormData()

  formData.append('id', String(id))
  formData.append('ownerId', pet.ownerId)
  formData.append('name', formState.name)

  if (formState.file) formData.append('uploaded_file', formState.file)
  else formData.append('imgUrl', formState.imgUrl)

  updatePetMutation.mutate(formData)
} 

In our case, the pet may already have an image with a URL saved in the db, so we first check if it exists and if not we send the file, otherwise we continue with the URL that was already there.

In the form itself (in the return part of the component), we also have to add the encryption tag just as it is written here and make sure the submit function is on the form, not on the button onClick:

<form enctype="multipart/form-data" onSubmit="{handleSubmit}"></form>

Setup client mutation

Just a minor change here to keep TypeScript happy - instead of your custom type, it should be of type FormData.

const queryClient = useQueryClient()

const updatePetMutation = useMutation({ 
  mutationFn: (pet: FormData) => updatePet(Number(id), pet), 
  onSuccess: async () => { 
    queryClient.invalidateQueries({ queryKey: ['updatePet', Number(id)] })
    setFormVisible(!formVisible)
  }, 
})

Setup client API

No major changes here, just change the custom type to the FormData type.

export async function updatePet(id: number, pet: FormData) { 
  const response = await request.put(`/api/v1/pets/${id}`).send(pet)
  return response.body as PetData
}

Fin

If all goes well, you should be able to change the fields of your forms and see the uploaded file in your public folder after the user hits submit. Verify it for yourself by refreshing the view of your database.