Handling file uploads in forms using Express
By Flavio Copes
Learn how to handle file uploads from forms in Express, sending data as multipart/form-data and parsing the uploaded files with the formidable library.
This is an example of an HTML form that allows a user to upload a file:
<form method="POST" action="/submit-form" enctype="multipart/form-data">
<input type="file" name="document" />
<input type="submit" />
</form>
Don’t forget to add
enctype="multipart/form-data"to the form, or files won’t be uploaded
When the user press the submit button, the browser will automatically make a POST request to the /submit-form URL on the same origin of the page. The browser sends the data contained, not encoded as as a normal form application/x-www-form-urlencoded , but as multipart/form-data.
Server-side, handling multipart data can be tricky and error prone, so we are going to use a utility library called formidable. Here’s the GitHub repo, it is well-maintained. This post uses formidable 3 (3.5.4 as of September 2026).
You can install it using:
npm install formidable
Then include it in your Express app. formidable 3 works with both import and require. Here I use ES modules, so the file needs "type": "module" in package.json or a .mjs extension:
import express from 'express'
import { formidable } from 'formidable'
const app = express()
Now, in the POST endpoint on the /submit-form route, we create a form parser with formidable():
app.post('/submit-form', (req, res) => {
const form = formidable({})
})
After doing so, we need to be able to parse the form. We can do so by providing a callback, which means all files are processed, and once formidable is done, it makes them available:
app.post('/submit-form', (req, res) => {
const form = formidable({})
form.parse(req, (err, fields, files) => {
if (err) {
console.error('Error', err)
throw err
}
console.log('Fields', fields)
console.log('Files', files)
for (const [name, fileList] of Object.entries(files)) {
console.log(name, fileList)
}
res.end()
})
})
In formidable 3, fields and files values are arrays. A single file input named document lands in files.document[0].
Or, you can use events instead of a callback. For example, to be notified when each file is parsed, or other events such as completion of file processing, receiving a non-file field, or if an error occurred.
Register the listeners on the form first, then call parse(). In formidable 3, parse(req) called without a callback returns a promise, not the form, so you can’t chain .on() after it:
app.post('/submit-form', (req, res) => {
const form = formidable({})
form
.on('field', (name, field) => {
console.log('Field', name, field)
})
.on('file', (name, file) => {
console.log('Uploaded file', name, file)
})
.on('aborted', () => {
console.error('Request aborted by the user')
})
.on('error', (err) => {
console.error('Error', err)
throw err
})
.on('end', () => {
res.end()
})
form.parse(req)
})
Whichever way you choose, you’ll get one or more file objects, which give you information about the file uploaded. These are some of the properties you can read:
file.size, the file size in bytesfile.filepath, the path the file is written tofile.originalFilename, the name of the file sent by the browserfile.mimetype, the MIME type of the file
The path defaults to the temporary folder and can be modified if you listen for the fileBegin event. In an ES module there is no __dirname, so I use import.meta.dirname (Node.js 20.11+) to build the path:
app.post('/submit-form', (req, res) => {
const form = formidable({})
form
.on('fileBegin', (name, file) => {
file.filepath = import.meta.dirname + '/uploads/' + file.originalFilename
})
.on('file', (name, file) => {
console.log('Uploaded file', name, file)
})
//...
form.parse(req)
})
Once you have the upload working, you will often want to validate the input before you trust anything on disk.
Want me to talk about your product? You can sponsor this site.