File Uploads in Express.js: A Complete Guide to Multer

Introduction: The Problem With Uploading Files
Think about every time you have uploaded something on the internet. A profile picture on a social media platform. A PDF resume on a job application website. A set of product images on an e-commerce store. These all look simple from the user's perspective. Click a button, select a file, done.
But from the server's perspective, receiving a file is a fundamentally different operation from receiving regular form data or JSON. And understanding why that difference exists is the first step to handling file uploads correctly in your Express applications.
When a regular HTML form is submitted with text fields, the data travels to the server in a simple format. The name and value of each field are paired together and sent as a string. Something like:
username=alice&email=alice@example.com&age=30
That is straightforward text. Express can parse it easily with the built-in express.urlencoded() middleware.
But a file is not text. A file is binary data. It could be image pixels, compressed document bytes, audio samples, or any other kind of raw data. You cannot represent a JPEG image as a simple key-value string the way you represent a username. The entire format of the data needs to be different.
This is where multipart form data comes in, and it is the foundation of everything we will cover in this article.
Part 1: Understanding Multipart Form Data
When an HTML form includes a file input and is submitted, the browser does not use the simple application/x-www-form-urlencoded format. Instead, it switches to a format called multipart/form-data.
The word multipart describes exactly what this format does. It divides the request body into multiple parts, where each part represents one piece of data from the form. Text fields become one part each. File inputs become another part, but this part contains the raw file data along with metadata like the original filename and the file type.
Here is a simplified look at what a multipart request body actually looks like:
--boundary12345
Content-Disposition: form-data; name="username"
alice
--boundary12345
Content-Disposition: form-data; name="profile_picture"; filename="photo.jpg"
Content-Type: image/jpeg
[raw binary data of the JPEG image goes here]
--boundary12345--
Each section is separated by a boundary string. The browser generates this boundary string and tells the server what it is via the Content-Type header:
Content-Type: multipart/form-data; boundary=boundary12345
The server needs to know about this boundary so it can split the body into its separate parts and read each one correctly.
This parsing is not trivial. Reading binary data from a stream, splitting it correctly by the boundary, identifying which parts are text and which are files, and handling the metadata of each part — this is a meaningful amount of work. Doing it yourself from scratch every time would be time-consuming and error-prone.
This is exactly why middleware exists for this purpose. And the most widely used middleware for handling multipart form data in Express is Multer.
Part 2: What Multer Is
Multer is a Node.js middleware specifically designed to handle multipart/form-data requests. It sits between the incoming request and your route handler, reads the multipart body, separates the file data from the text field data, and makes everything available to you in a clean, organized way.
Without Multer, if someone sends a multipart request to your Express server, req.body would be empty or undefined even if the request contained data. Multer processes the raw stream and populates req.body with the text fields and req.file (or req.files for multiple files) with the uploaded file information.
Multer does not do anything with the files that goes beyond what you configure it to do. It does not save files to any particular location by default, it does not validate file types, and it does not impose size limits unless you tell it to. This design keeps Multer minimal and gives you full control over how uploads are handled.
Installing Multer
In your project directory, install Multer using npm:
npm install multer
After installation, you bring it into your file with require:
const multer = require('multer');
Part 3: The Upload Lifecycle
Before writing any code, it helps to have a clear picture of what happens from the moment a user selects a file to the moment your route handler has access to it.
User selects a file in the browser
|
v
Browser creates a multipart/form-data request
|
v
Request arrives at your Express server
|
v
Multer middleware reads the request stream
|
v
Multer separates text fields from file data
|
v
Multer processes the file according to your storage configuration
|
v
req.body is populated with text fields
req.file (or req.files) is populated with file information
|
v
Your route handler function runs
|
v
You send a response to the client
Every step in this chain matters. If Multer is not attached to the route, the file never gets processed. If your storage is not configured, Multer defaults to keeping the file in memory. If you do not check req.file, you do not know if a file was actually received.
Understanding this lifecycle will make it much easier to debug issues when they arise.
Part 4: Setting Up Basic Multer Configuration
There are two ways to configure where Multer puts files: memory storage and disk storage.
Memory Storage
With memory storage, Multer keeps the uploaded file in memory as a Buffer object. It never touches the file system. The file data lives in RAM until your code does something with it.
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() });
Memory storage is simple to set up and useful when you intend to process the file data immediately and then either discard it or pass it somewhere else, like a cloud storage service. It is not appropriate for large files because keeping large amounts of data in RAM can exhaust your server's memory.
Disk Storage
Disk storage saves the uploaded file to the file system on your server. This is what you will use in most basic applications. You configure where the file goes and what it gets named.
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: function(req, file, callback) {
callback(null, 'uploads/');
},
filename: function(req, file, callback) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const extension = path.extname(file.originalname);
callback(null, uniqueSuffix + extension);
}
});
const upload = multer({ storage: storage });
Let us go through each part of this configuration.
multer.diskStorage() takes a configuration object with two properties: destination and filename. Both are functions that Multer calls when processing an upload.
The destination function:
destination: function(req, file, callback) {
callback(null, 'uploads/');
}
This function receives the request object, the file object (containing metadata about the uploaded file), and a callback. You call the callback with two arguments: an error (null if there is no error) and the path to the directory where the file should be saved.
The directory must already exist. Multer does not create directories for you. If the uploads/ directory does not exist when a file is uploaded, Multer will throw an error.
The filename function:
filename: function(req, file, callback) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const extension = path.extname(file.originalname);
callback(null, uniqueSuffix + extension);
}
This function determines what name the saved file gets. You call the callback with an error (null if none) and the filename string.
The reason for generating a unique name rather than using file.originalname directly is important. If two users upload files with the same name — two users both upload profile.jpg for instance — and you save both with the original name, the second upload overwrites the first. Generating a unique name based on the current timestamp and a random number prevents this collision.
path.extname(file.originalname) extracts the file extension from the original filename. So if the original file was vacation.jpg, path.extname returns .jpg, and the saved file might be named something like 1703001234567-892341567.jpg.
The file Object Available in Callbacks
Inside both the destination and filename functions, you have access to a file object that contains information about the file being uploaded:
file.fieldname The name of the form field (e.g., 'profile_picture')
file.originalname The original name of the file on the user's computer
file.encoding The encoding type of the file
file.mimetype The MIME type of the file (e.g., 'image/jpeg')
You can use these properties to make decisions. For example, you could check file.mimetype in the destination function to save images in one folder and documents in another.
Part 5: Handling a Single File Upload
Let us build a complete working example of single file upload from start to finish.
Setting Up the Project
First, create the upload directory. Multer needs it to exist:
mkdir uploads
Then create your server file:
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const PORT = 3000;
// Configure storage
const storage = multer.diskStorage({
destination: function(req, file, callback) {
callback(null, 'uploads/');
},
filename: function(req, file, callback) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const extension = path.extname(file.originalname);
callback(null, uniqueSuffix + extension);
}
});
const upload = multer({ storage: storage });
// Serve an HTML form so we can test the upload
app.get('/', function(req, res) {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>File Upload</title>
</head>
<body>
<h1>Upload a File</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<div>
<label>Your name:</label>
<input type="text" name="username" />
</div>
<div>
<label>Choose a file:</label>
<input type="file" name="uploaded_file" />
</div>
<button type="submit">Upload</button>
</form>
</body>
</html>
`);
});
// Handle the upload
app.post('/upload', upload.single('uploaded_file'), function(req, res) {
// If no file was uploaded
if (!req.file) {
return res.status(400).json({
success: false,
error: 'No file was uploaded'
});
}
// req.body contains the text fields
// req.file contains the uploaded file information
res.json({
success: true,
message: 'File uploaded successfully',
submittedBy: req.body.username,
file: {
originalName: req.file.originalname,
savedAs: req.file.filename,
size: req.file.size,
mimeType: req.file.mimetype,
path: req.file.path
}
});
});
app.listen(PORT, function() {
console.log('Server running on port ' + PORT);
});
Understanding upload.single()
The key line in the route definition is:
app.post('/upload', upload.single('uploaded_file'), function(req, res) {
upload.single('uploaded_file') is the Multer middleware for single file uploads. The string argument 'uploaded_file' is the name of the form field that contains the file. This must match the name attribute on your file input element exactly:
<input type="file" name="uploaded_file" />
If the name does not match, Multer will not find the file and req.file will be undefined.
When this middleware runs, it processes the uploaded file and attaches information about it to req.file.
What req.file Contains
After a successful single file upload, req.file is an object with these properties:
fieldname The field name from the form ('uploaded_file' in our example)
originalname The original filename from the user's computer
encoding The encoding of the file
mimetype The MIME type (e.g., 'image/jpeg', 'application/pdf')
destination The folder where the file was saved ('uploads/')
filename The name Multer gave the file in the destination folder
path The full path to the saved file ('uploads/uniquename.jpg')
size The file size in bytes
If no file was included in the request, req.file is undefined. Always check for this before trying to use req.file.
Testing the Upload
Start the server and visit http://localhost:3000. Fill in a name, choose a file, and click Upload. You will see a JSON response showing the file details. Check your uploads/ directory and you will find the file there with its generated unique name.
Part 6: Handling Multiple File Uploads
Many real applications need to accept more than one file at a time. A property listing might accept multiple photos. A document submission form might accept several attachments. Multer handles this with the upload.array() and upload.fields() methods.
upload.array() — Multiple Files From One Field
upload.array() accepts multiple files from a single file input that allows multiple selections:
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const storage = multer.diskStorage({
destination: function(req, file, callback) {
callback(null, 'uploads/');
},
filename: function(req, file, callback) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const extension = path.extname(file.originalname);
callback(null, uniqueSuffix + extension);
}
});
const upload = multer({ storage: storage });
// Serve a form with multiple file selection
app.get('/', function(req, res) {
res.send(`
<!DOCTYPE html>
<html>
<head><title>Multiple File Upload</title></head>
<body>
<h1>Upload Multiple Files</h1>
<form action="/upload-multiple" method="POST" enctype="multipart/form-data">
<div>
<label>Album name:</label>
<input type="text" name="albumName" />
</div>
<div>
<label>Select photos (you can select multiple):</label>
<input type="file" name="photos" multiple />
</div>
<button type="submit">Upload Photos</button>
</form>
</body>
</html>
`);
});
// Handle multiple file upload
// Second argument to array() is the maximum number of files allowed
app.post('/upload-multiple', upload.array('photos', 10), function(req, res) {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
error: 'No files were uploaded'
});
}
const fileDetails = req.files.map(function(file) {
return {
originalName: file.originalname,
savedAs: file.filename,
size: file.size,
mimeType: file.mimetype
};
});
res.json({
success: true,
message: req.files.length + ' files uploaded successfully',
albumName: req.body.albumName,
files: fileDetails
});
});
app.listen(3000, function() {
console.log('Server running on port 3000');
});
upload.array('photos', 10) takes two arguments: the field name and the maximum number of files to accept. If more than the maximum are uploaded, Multer returns an error.
With multiple files, req.files (note the plural) is an array of file objects, each with the same structure as req.file in a single upload. req.file (singular) is undefined when using upload.array().
upload.fields() — Multiple Files From Multiple Fields
Sometimes a form has separate file inputs for different purposes. A user registration form might have separate fields for a profile picture and an identity document. upload.fields() handles this:
app.get('/register', function(req, res) {
res.send(`
<!DOCTYPE html>
<html>
<head><title>Register</title></head>
<body>
<h1>Register with Documents</h1>
<form action="/register" method="POST" enctype="multipart/form-data">
<div>
<label>Full name:</label>
<input type="text" name="fullName" />
</div>
<div>
<label>Profile picture:</label>
<input type="file" name="profilePicture" />
</div>
<div>
<label>Identity document:</label>
<input type="file" name="identityDocument" />
</div>
<button type="submit">Register</button>
</form>
</body>
</html>
`);
});
const uploadFields = upload.fields([
{ name: 'profilePicture', maxCount: 1 },
{ name: 'identityDocument', maxCount: 1 }
]);
app.post('/register', uploadFields, function(req, res) {
const profilePicture = req.files['profilePicture'];
const identityDocument = req.files['identityDocument'];
if (!profilePicture) {
return res.status(400).json({ error: 'Profile picture is required' });
}
if (!identityDocument) {
return res.status(400).json({ error: 'Identity document is required' });
}
res.json({
success: true,
message: 'Registration files received',
fullName: req.body.fullName,
profilePicture: {
originalName: profilePicture[0].originalname,
savedAs: profilePicture[0].filename,
size: profilePicture[0].size
},
identityDocument: {
originalName: identityDocument[0].originalname,
savedAs: identityDocument[0].filename,
size: identityDocument[0].size
}
});
});
With upload.fields(), req.files is an object where each key is a field name and each value is an array of file objects for that field. Even if you specified maxCount: 1, the value is still an array, so you access the first file with req.files['fieldname'][0].
Choosing the Right Method
upload.single('fieldname') One file from one field
upload.array('fieldname', max) Multiple files from one field
upload.fields([...]) Files from multiple different fields
upload.none() No files, only text fields
Part 7: Organizing Uploads With a Folder Structure
As your application grows, putting every uploaded file into a single uploads/ folder becomes difficult to manage. A cleaner approach is to organize files into subfolders based on their type or purpose.
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
// Create subdirectories if they do not exist
const directories = ['uploads/images', 'uploads/documents', 'uploads/other'];
directories.forEach(function(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
const storage = multer.diskStorage({
destination: function(req, file, callback) {
// Determine the subdirectory based on MIME type
let uploadPath = 'uploads/other';
if (file.mimetype.startsWith('image/')) {
uploadPath = 'uploads/images';
} else if (file.mimetype === 'application/pdf' ||
file.mimetype === 'application/msword' ||
file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
uploadPath = 'uploads/documents';
}
callback(null, uploadPath);
},
filename: function(req, file, callback) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const extension = path.extname(file.originalname).toLowerCase();
callback(null, uniqueSuffix + extension);
}
});
const upload = multer({ storage: storage });
app.post('/upload', upload.single('file'), function(req, res) {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
success: true,
file: {
originalName: req.file.originalname,
savedAs: req.file.filename,
location: req.file.path,
size: req.file.size,
type: req.file.mimetype
}
});
});
app.listen(3000, function() {
console.log('Server running on port 3000');
console.log('Upload structure:');
console.log(' uploads/images - for image files');
console.log(' uploads/documents - for PDF and Word files');
console.log(' uploads/other - for everything else');
});
The destination function uses file.mimetype to decide which subdirectory the file belongs in. Images go to uploads/images, documents go to uploads/documents, and everything else goes to uploads/other. The folders are created automatically at startup using fs.mkdirSync with the recursive: true option, which creates parent directories as needed.
Your project structure after running this and uploading some files would look like:
project/
uploads/
images/
1703001234567-892341567.jpg
1703001298765-123456789.png
documents/
1703001345678-987654321.pdf
other/
1703001400000-111111111.zip
server.js
package.json
Part 8: Adding File Filters and Size Limits
Accepting any file that anyone wants to upload is rarely what you want. Most applications need to restrict uploads to specific file types and reasonable sizes.
File Type Filtering
Multer accepts a fileFilter option in its configuration. This is a function that runs for each incoming file and decides whether to accept or reject it:
const imageFilter = function(req, file, callback) {
// Accept only image files
const allowedMimeTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp'
];
if (allowedMimeTypes.includes(file.mimetype)) {
// Accept the file
callback(null, true);
} else {
// Reject the file with an error
callback(new Error('Only image files are allowed. Received: ' + file.mimetype), false);
}
};
const upload = multer({
storage: storage,
fileFilter: imageFilter
});
The fileFilter function receives the request, the file object, and a callback. Call the callback with (null, true) to accept the file. Call it with (error, false) to reject it. When you pass an error, Multer passes that error to your error handler.
You can also check the file extension as an additional measure, though MIME type is generally more reliable:
JavaScript
const documentFilter = function(req, file, callback) { const allowedExtensions = ['.pdf', '.doc', '.docx']; const fileExtension = path.extname(file.originalname).toLowerCase();
if (allowedExtensions.includes(fileExtension)) {
callback(null, true);
} else {
callback(new Error('Only PDF and Word documents are allowed'), false);
}
};
File Size Limits Multer accepts a limits option that lets you restrict file sizes and quantities:
JavaScript
const upload = multer({ storage: storage, fileFilter: imageFilter, limits: { fileSize: 5 * 1024 * 1024, // 5 megabytes in bytes files: 10 // Maximum 10 files in a single request } }); fileSize is specified in bytes. The calculation 5 * 1024 * 1024 gives you 5 megabytes. 1024 bytes is one kilobyte, 1024 * 1024 bytes is one megabyte, so 5 * 1024 * 1024 is five megabytes.
If an uploaded file exceeds the size limit, Multer rejects it with an error that you can catch and handle.
Handling Multer Errors
When Multer encounters an error — a file that fails the filter, a file that is too large — it passes that error through the Express middleware chain. You need to handle it explicitly, because Multer errors are instances of multer.MulterError, which is a specific class you can check for:
app.post('/upload', function(req, res, next) {
upload.single('image')(req, res, function(err) {
if (err instanceof multer.MulterError) {
// A Multer-specific error occurred
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
error: 'File is too large. Maximum size is 5MB.'
});
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({
error: 'Too many files uploaded at once.'
});
}
return res.status(400).json({
error: 'File upload error: ' + err.message
});
}
if (err) {
// A non-Multer error occurred (like from our fileFilter)
return res.status(400).json({
error: err.message
});
}
// No error - proceed with the upload
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
success: true,
file: {
originalName: req.file.originalname,
size: req.file.size,
type: req.file.mimetype
}
});
});
});
This pattern — calling the Multer middleware manually inside the route handler and passing a callback — gives you full control over error handling. The callback receives any error that Multer encountered. You check if it is a MulterError instance, check the error code, and respond accordingly.
Common Multer error codes:
LIMIT_PART_COUNT Too many parts in the multipart form
LIMIT_FILE_SIZE File exceeds the size limit
LIMIT_FILE_COUNT Too many files uploaded
LIMIT_FIELD_KEY Field name is too long
LIMIT_FIELD_VALUE Field value is too long
LIMIT_FIELD_COUNT Too many fields in the form
LIMIT_UNEXPECTED_FILE An unexpected field name was encountered
Part 9: Serving Uploaded Files
Saving files to a server is only half the picture. Users also need to be able to access those files via a URL. If someone uploads a profile picture, they need to be able to see it displayed on their profile page.
Express has a built-in mechanism for serving files from a directory, called static file serving. You configure it with express.static():
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// Make the uploads folder publicly accessible
// Files in 'uploads/' can be accessed at /uploads/filename
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
After this line, any file in your uploads/ directory is accessible via a URL. If a file is saved at uploads/images/1703001234567-892341567.jpg, it becomes accessible at:
http://localhost:3000/uploads/images/1703001234567-892341567.jpg
The first argument to app.use() is the URL prefix. The argument to express.static() is the actual directory path on the file system. Express maps one to the other, so requests to /uploads/... are served from the uploads/ directory.
Building and Returning File URLs
When a file is uploaded, you should build and return the full URL so the client knows where to access it:
app.post('/upload', upload.single('image'), function(req, res) {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Build the URL where this file can be accessed
const fileUrl = req.protocol + '://' + req.get('host') + '/uploads/' + req.file.filename;
res.json({
success: true,
message: 'File uploaded successfully',
url: fileUrl,
filename: req.file.filename
});
});
req.protocol gives you http or https. req.get('host') gives you the hostname and port, like localhost:3000. Together they form the base URL, and you append the path to the file.
The client receives the URL in the response and can use it directly in an image tag, a link, or store it in a database for later use.
A Complete Upload and Serve Example
Here is a complete server that handles image uploads, serves them via URL, and displays them:
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
const PORT = 3000;
// Create uploads directory if it does not exist
if (!fs.existsSync('uploads')) {
fs.mkdirSync('uploads');
}
// Storage configuration
const storage = multer.diskStorage({
destination: function(req, file, callback) {
callback(null, 'uploads/');
},
filename: function(req, file, callback) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const extension = path.extname(file.originalname).toLowerCase();
callback(null, uniqueSuffix + extension);
}
});
// File filter for images only
const imageFilter = function(req, file, callback) {
const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (allowed.includes(file.mimetype)) {
callback(null, true);
} else {
callback(new Error('Only image files are accepted'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: imageFilter,
limits: { fileSize: 5 * 1024 * 1024 }
});
// Serve uploaded files
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Upload form
app.get('/', function(req, res) {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Image Upload</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; }
img { max-width: 300px; margin-top: 20px; display: block; }
</style>
</head>
<body>
<h1>Upload an Image</h1>
<form id="uploadForm" action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" />
<button type="submit">Upload</button>
</form>
<div id="result"></div>
<script>
document.getElementById('uploadForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
const resultDiv = document.getElementById('result');
if (data.success) {
resultDiv.innerHTML =
'<p>Uploaded successfully.</p>' +
'<p>URL: ' + data.url + '</p>' +
'<img src="' + data.url + '" alt="Uploaded image" />';
} else {
resultDiv.innerHTML = '<p style="color:red">Error: ' + data.error + '</p>';
}
});
</script>
</body>
</html>
`);
});
// Handle upload
app.post('/upload', function(req, res) {
upload.single('image')(req, res, function(err) {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
success: false,
error: 'Image is too large. Maximum size is 5MB.'
});
}
return res.status(400).json({
success: false,
error: 'Upload error: ' + err.message
});
}
if (err) {
return res.status(400).json({
success: false,
error: err.message
});
}
if (!req.file) {
return res.status(400).json({
success: false,
error: 'Please select an image to upload'
});
}
const fileUrl = req.protocol + '://' + req.get('host') + '/uploads/' + req.file.filename;
res.json({
success: true,
url: fileUrl,
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size
});
});
});
// 404 handler
app.use(function(req, res) {
res.status(404).json({ error: 'Route not found' });
});
app.listen(PORT, function() {
console.log('Server running at http://localhost:' + PORT);
});
Visit http://localhost:3000, select an image, and click Upload. The page will show you the URL and display the uploaded image. The image is served directly from the uploads/ directory through the static file middleware.
Part 10: Listing and Managing Uploaded Files
A complete upload system usually needs to list existing files and possibly delete them. Here is how you add those capabilities:
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
const storage = multer.diskStorage({
destination: function(req, file, callback) {
callback(null, 'uploads/');
},
filename: function(req, file, callback) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const extension = path.extname(file.originalname).toLowerCase();
callback(null, uniqueSuffix + extension);
}
});
const upload = multer({ storage: storage });
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Upload a file
app.post('/files', upload.single('file'), function(req, res) {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const fileUrl = req.protocol + '://' + req.get('host') + '/uploads/' + req.file.filename;
res.status(201).json({
success: true,
file: {
name: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
url: fileUrl
}
});
});
// List all uploaded files
app.get('/files', function(req, res) {
const uploadDir = path.join(__dirname, 'uploads');
fs.readdir(uploadDir, function(err, files) {
if (err) {
return res.status(500).json({ error: 'Could not read uploads directory' });
}
const baseUrl = req.protocol + '://' + req.get('host');
const fileList = files.map(function(filename) {
const filePath = path.join(uploadDir, filename);
const stats = fs.statSync(filePath);
return {
name: filename,
size: stats.size,
uploadedAt: stats.birthtime,
url: baseUrl + '/uploads/' + filename
};
});
res.json({
count: fileList.length,
files: fileList
});
});
});
// Delete a specific file
app.delete('/files/:filename', function(req, res) {
const filename = req.params.filename;
// Prevent directory traversal attacks
// Only allow simple filenames, no path separators
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
return res.status(400).json({ error: 'Invalid filename' });
}
const filePath = path.join(__dirname, 'uploads', filename);
fs.access(filePath, fs.constants.F_OK, function(accessErr) {
if (accessErr) {
return res.status(404).json({ error: 'File not found' });
}
fs.unlink(filePath, function(unlinkErr) {
if (unlinkErr) {
return res.status(500).json({ error: 'Could not delete file' });
}
res.json({
success: true,
message: 'File deleted successfully',
filename: filename
});
});
});
});
app.listen(3000, function() {
console.log('Server running on port 3000');
});
The list endpoint reads the uploads directory with fs.readdir and returns information about each file including its URL. The delete endpoint removes a file from the filesystem with fs.unlink, after checking that the file exists and that the filename does not contain path traversal sequences that could be used to delete files outside the uploads directory.
Summary
Here is a concise recap of everything covered in this article.
Why file uploads need middleware: File uploads use multipart/form-data encoding, which sends binary file data mixed with form fields separated by boundaries. Parsing this format manually is complex, which is why dedicated middleware exists.
What Multer is: Multer is a Node.js middleware for handling multipart form data. It parses incoming file uploads and makes the file information available on req.file or req.files, and text field data available on req.body.
Storage configuration: Multer supports memory storage (keeps file in RAM as a Buffer) and disk storage (saves file to the filesystem). Disk storage is configured with a destination function and a filename function, both receiving the request, file metadata, and a callback.
Single file upload: Use upload.single('fieldname') as route middleware. Access the uploaded file via req.file.
Multiple file uploads: Use upload.array('fieldname', maxCount) for multiple files from one field, accessing them via req.files. Use upload.fields([...]) for files from multiple different fields, accessing them via req.files['fieldname'].
File filters and limits: Pass a fileFilter function to validate file types before accepting them. Use the limits option to restrict file size and count. Handle Multer errors by calling the middleware manually and checking for multer.MulterError instances.
Serving uploaded files: Use app.use('/uploads', express.static(path.join(__dirname, 'uploads'))) to make uploaded files accessible via URL. Build the file URL from req.protocol, req.get('host'), and the filename to return to the client.






