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:
- commonJS vs ES modules
- axios or fetch vs superagent and tanstack react query and express router
- form that allows only file upload vs form that also has other text fields
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:
importalways goes to the top of the file.- The reason
storageanduploadare at the top of the file is so that any other functions can use them. destinationtells the computer where the files will be stored when the user uploads them.-
filenametells the computer how to name the file. If this is not specified, the uploaded file will be all numbers and have no extension, so then your front end won't be able to display it, if it's an image, for example. Date.now()just prefixes the file with the date/time the image was uploaded.
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.