Storing Uploaded Files and Serving Them in Express

Users need to upload files to your application. Maybe they're uploading profile pictures, documents, or media. Once you store these files, you need to serve them back to users. This requires understanding where files go and how to make them accessible.
Where Do Uploaded Files Live?
When a user uploads a file, it doesn't magically appear on the web. You need to decide where to store it.
There are two main approaches:
Local Storage (on your server): Files are saved to a folder on your server's disk. Your server then serves these files directly to users.
External Storage (cloud): Files are uploaded to a service like AWS S3, Google Cloud Storage, or Cloudinary. Your server doesn't store them; the cloud service does.
We'll focus on local storage first, as it's simpler to understand and perfect for learning.
Setting Up a Local Upload Folder
Create a folder where uploads will be stored:
project/
uploads/
server.js
package.json
This uploads folder holds all user files.
Handling File Uploads with Multer
Express doesn't handle file uploads by default. You need middleware like Multer.
First, install Multer:
npm install multer
Here's how to set up basic file uploads:
import express from 'express';
import multer from 'multer';
import path from 'path';
import { fileURLToPath } from 'url';
const app = express();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({ storage });
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).send('No file uploaded');
}
res.json({ message: 'File uploaded', filename: req.file.filename });
});
app.listen(3000, () => console.log('Server running'));
When a user uploads a file:
Multer intercepts the request
Saves the file to the
uploads/folder with a unique nameAdds file information to
req.fileYour route handler processes the request
Serving Uploaded Files
Now users need to access these files. You'll serve them as static files.
Express has a built-in middleware for this:
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
const app = express();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Serve files from uploads folder
app.use('/files', express.static(path.join(__dirname, 'uploads')));
app.listen(3000, () => console.log('Server running'));
Now files are accessible at http://localhost:3000/files/filename.jpg
If a file is saved as profile-1234567890.jpg in the uploads/ folder, it's accessible at:
http://localhost:3000/files/profile-1234567890.jpg
Complete Upload and Serve Example
Here's a complete example with upload and serving:
import express from 'express';
import multer from 'multer';
import path from 'path';
import { fileURLToPath } from 'url';
const app = express();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({ storage });
// Serve uploaded files as static
app.use('/files', express.static(path.join(__dirname, 'uploads')));
app.post('/upload-profile', upload.single('profilePic'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const fileUrl = `/files/${req.file.filename}`;
res.json({
message: 'Profile picture uploaded',
url: fileUrl
});
});
app.listen(3000, () => console.log('Server running'));
A user uploads a file → Multer saves it → Server returns the file URL → User can access it immediately.
Upload Process Visualization
Client Server Disk
| | |
|--- POST /upload (file) -----> | |
| (multipart/form-data) | |
| |-- Multer processes ------>|
| | saves file |
| | |
|<-- 200 OK + filename ---------| |
| { url: '/files/...' } | |
| | |
|-- GET /files/filename.jpg --> | |
| |-- Read from disk ------> |
|<-- 200 + file data -----------|<-- Return file data ------|
Security Considerations
Validate file types:
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
Limit file size:
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 } // 5MB
});
Never trust the original filename:
Always generate a new filename (like we did with timestamps and random numbers). If you use the original filename, malicious users could upload files with paths like ../../important.js to write files outside the uploads folder.
Storing Metadata
Usually, you'll want to store information about the uploaded file:
import express from 'express';
import multer from 'multer';
import path from 'path';
import { fileURLToPath } from 'url';
const app = express();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
app.use(express.json());
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({ storage });
app.use('/files', express.static(path.join(__dirname, 'uploads')));
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Store metadata about the file
const fileData = {
originalName: req.file.originalname,
savedName: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype,
uploadedAt: new Date(),
url: `/files/${req.file.filename}`
};
// You'd normally save fileData to a database
res.json(fileData);
});
app.listen(3000, () => console.log('Server running'));
Store this information in a database so you can track what was uploaded, when, and by whom.
Common Mistakes
Mistake 1: Using original filenames
Never save files with their original names. Attackers can upload files with special characters or path traversal sequences.
Mistake 2: Not validating file types
Don't just check the file extension. Someone can rename malware.exe to image.jpg. Always check the MIME type.
Mistake 3: Storing sensitive files in the web-accessible folder
Files served via express.static are publicly accessible. Don't store passwords, API keys, or sensitive data there.
Key Takeaways
Multer middleware handles file uploads in Express
Files are stored to disk in a designated folder
Use express.static middleware to serve uploaded files
Generate unique filenames to prevent security issues
Validate file types and sizes
Store file metadata in a database
Never trust the original filename provided by users
Once you're comfortable with local storage, you can explore cloud storage solutions for better scalability and reliability.



