Hello readers 👋, welcome to the 13th blog in our Node.js series!
In the last post, we learned how Multer helps us accept file uploads in Express, whether single files or multiple files. We configured a disk storage engine and saw how req.file and req.files give us all the details about what arrived. But one question remained unanswered: where do the uploaded files actually live, and how do clients get them back?
Today we're going to close that loop. We'll talk about storage choices, how to serve uploaded files so they're accessible via a URL, and crucial security practices that keep your application safe. By the end, you'll have a complete picture of the upload‑serve lifecycle.
Let's jump in.
Where uploaded files are stored
When Multer saves a file on the server, it places it into a folder that you specify via the destination callback. In our previous example, we used an uploads folder at the root of our project. After a few uploads, the folder might look something like this:
project/
├── uploads/
│ ├── avatar-1680000000-123456789.png
│ ├── avatar-1680000010-987654321.jpg
│ └── cover-1680000020-456789123.jpeg
├── server.js
└── package.json
The actual filenames are generated by the filename callback (often using a timestamp and a random number to avoid collisions). The original name is still available inside req.file.originalname, but on disk we keep a sanitized, unique name.
Local storage vs external storage concept
In development and small applications, storing files on the local filesystem is perfectly fine. It's simple and needs no extra setup. But as your application grows, you'll often hear about external storage services like Amazon S3, Google Cloud Storage, or Cloudinary. These services store files separately from your Node.js server, which brings several benefits:
- Your server's disk space isn't used by user files.
- The files can be served to clients directly from a CDN, reducing load on your application server.
- Scaling becomes easier because the storage layer is independent.
For this post, we'll focus entirely on local storage because that's where most people start. We'll mention how to serve those local files directly to clients. Later, when you move to cloud storage, the ideas of URLs and security transfer easily.
Serving static files in Express
After a file is uploaded and stored on disk, you typically want to make it accessible through a URL so that a client (browser, mobile app) can download or display it. Express provides a built‑in middleware called express.static exactly for this purpose.
express.static takes a folder path and serves all files inside it as static assets. For example:
const express = require('express');
const app = express();
// Serve files from the 'uploads' folder at the '/files' URL path
app.use('/files', express.static('uploads'));
Now, if there is a file uploads/avatar-123456.png, it becomes available at http://localhost:3000/files/avatar-123456.png. The URL path you choose (/files) can be anything; it doesn't have to match the folder name.
A common practice is to serve the uploads folder under a dedicated path like /uploads or /files to keep the URL structure clear.
Setting up the complete flow
Let's combine Multer and static serving into a single Express app to see the full picture:
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
const port = 3000;
// Ensure uploads folder exists
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir);
}
// Multer configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, 'uploads/'),
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
const upload = multer({ storage });
// Serve uploaded files statically
app.use('/files', express.static('uploads'));
// Upload route
app.post('/upload', upload.single('photo'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Build the public URL
const fileUrl = `http://localhost:${port}/files/${req.file.filename}`;
res.json({
message: 'File uploaded successfully',
url: fileUrl
});
});
app.listen(port, () => console.log(`Server running on port ${port}`));
After uploading, the client receives a JSON response containing the direct URL. It can then use that URL to display an image, download a document, or simply check the file.
Accessing uploaded files via URL
The key insight is that the public URL is built from three parts:
-
Base URL of your server (e.g.,
http://localhost:3000) -
Static serving path (e.g.,
/files) - Filename on disk (the unique name generated by Multer)
In the route handler, req.file.filename gives us the exact name stored on disk. Concatenating these pieces gives a valid URL that Express will serve from the uploads folder.
For a more production‑ready approach, you'd likely construct the URL from a configuration setting, not hardcode localhost. For example:
const baseUrl = process.env.BASE_URL || `http://localhost:${port}`;
const fileUrl = `${baseUrl}/files/${req.file.filename}`;
Now clients always get the correct URL regardless of the environment.
Security considerations for uploads
File uploads are powerful but also a potential attack vector if not handled carefully. Here are some essential safe‑handling practices that every developer should follow.
1. Limit allowed file types
Never accept arbitrary file types. Use Multer's fileFilter to check the MIME type or file extension before writing to disk. For example, if you expect images:
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
const mimeType = allowedTypes.test(file.mimetype);
const extName = allowedTypes.test(path.extname(file.originalname).toLowerCase());
if (mimeType && extName) {
return cb(null, true);
}
cb(new Error('Only image files are allowed'));
}
});
2. Restrict file size
You can set a limits object on the Multer instance to reject files that are too large:
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 } // 5 MB
});
This prevents someone from filling up your server's disk with a single request.
3. Rename files to avoid path traversal
Never trust the original filename directly in the storage path. Always generate a new name, as we did with the filename callback. Using Date.now() and a random number makes it nearly impossible for an attacker to guess the filename. Also avoid including user‑supplied strings in the path to prevent directory traversal attacks (e.g., ../../../etc/passwd).
4. Store uploaded files outside the public web root
If possible, keep the uploads folder outside the main Express static folder that is openly served. In our setup, we used /files explicitly, but the uploads folder itself is not automatically exposed except through that route. Still, it's a good idea to store uploads in a directory that is not directly under the static root, and then use express.static with an absolute path to serve it under a specific URL prefix. This prevents direct access via path guessing.
5. Do not execute uploaded files
Make sure your static file serving middleware only serves files with safe content types. For example, if someone uploads a .php or .js file and you serve it via express.static, a browser might try to interpret it as code, especially if served with the wrong Content-Type. Express/Node.js static middleware sends files with an appropriate MIME type based on the extension, which usually prevents execution. Still, it's wise to limit accepted extensions and avoid storing executable scripts in the uploads folder.
6. Scan for malware (advanced)
For high‑security applications, you can integrate virus scanning tools that inspect uploaded files before they're stored. This is beyond the scope of this post, but keep it in mind for production systems.
Conclusion
Storing and serving uploaded files completes the file upload puzzle. By using Multer's disk storage and Express's static middleware, you can quickly give your users the ability to upload content and retrieve it via clean URLs. Add a few security precautions, and you have a robust foundation that works for development and small production apps.
Let's quickly recap:
- Uploaded files can be stored locally in a project folder (e.g.,
uploads/) or externally on cloud services for scalability. -
express.staticlets you serve the files under a URL prefix, making them accessible to clients. - Building a file URL combines the base URL, the static serving path, and the unique filename stored on disk.
- Security is essential: limit file types and sizes, rename files, avoid executable files, and store uploads outside the main public root if possible.
- A clear folder structure and the static middleware together create an intuitive upload‑serve flow.
Now you have the full cycle: receiving files and giving them back. In the next post, we'll explore something new, maybe working with databases or error handling. See you then!
Hope you found this helpful! If you spot any mistakes or have suggestions, let me know. You can find me on LinkedIn and X, where I post more about web development.
Top comments (0)