Storing Uploaded Files and Serving Them in Express

Every app lets users upload files. You need to store them and make them accessible. Local storage is simple but doesn't scale. Cloud storage scales but costs money. Pick wrong and you'll either run out of disk space or overspend.
This is about how to handle uploads, where to put files, and what not to do.
Where Uploaded Files Are Stored
You have two choices: store files on your server's disk, or send them to an external service.
Local Storage (Your Server's Disk)
project/
├── app.js
├── uploads/ <- Files end up here
│ ├── profile-pic.jpg
│ ├── document.pdf
│ └── resume.docx
└── public/
Files live in a folder on your server.
External Storage (Cloud Services)
Files go to AWS S3, Google Cloud Storage, Azure, etc. You store a link to the file instead of the file itself.
// You store a reference like this
const fileUrl = "https://s3.amazonaws.com/my-bucket/profile-pic.jpg";
Local Storage vs External Storage
Local Storage
Store files in a folder on your server.
Pros:
Simple to get working
No external dependencies
Free (just disk space)
Cons:
Disk fills up
Can't share files across multiple servers
Backups are manual
Doesn't scale
Use when:
You're developing or testing
You have one server
Files are small
You're learning how uploads work
External Storage (S3, Google Cloud, etc.)
Send files to a cloud service, get a URL back.
Pros:
Scales automatically
Handles backups for you
Multiple servers can access the same files
Fast delivery with CDN
Cons:
Costs money per GB
Requires configuration
More complexity
Need API keys
Use when:
You're in production
You expect high traffic
Files are large
You need to scale horizontally
Quick Comparison
| Feature | Local Storage | External (S3, etc.) |
|---|---|---|
| Setup | Minutes | 15-30 minutes |
| Cost | Free (storage cost on server) | Pay per GB |
| Scaling | Difficult | Automatic |
| Backup | Manual | Automatic |
| Retrieval | Direct file read | HTTP request |
| Multi-server | Doesn't work | Works perfectly |
| Best for | Development | Production |
Serving Static Files in Express
Express has a built-in way to serve files from a folder.
Basic Setup
const express = require("express");
const app = express();
// Serve everything in uploads folder
app.use(express.static("uploads"));
app.listen(3000);
Now any file in uploads/ is accessible by HTTP.
File: uploads/profile-pic.jpg
Access: http://localhost:3000/profile-pic.jpg
File: uploads/documents/resume.pdf
Access: http://localhost:3000/documents/resume.pdf
Handling Uploads with Multer
Use the multer middleware to receive and save files.
const express = require("express");
const multer = require("multer");
const app = express();
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
const uniqueName = Date.now() + "-" + file.originalname;
cb(null, uniqueName);
}
});
const upload = multer({ storage });
app.post("/upload", upload.single("file"), (req, res) => {
const fileUrl = `http://localhost:3000/${req.file.filename}`;
res.json({
filename: req.file.filename,
url: fileUrl
});
});
app.use(express.static("uploads"));
app.listen(3000);
What Happens
1. User sends file to /upload
2. Multer receives it
3. Multer saves it to uploads/
4. Multer adds file info to req.file
5. You send back the URL
6. User can access it via that URL
Upload and Storage Flow
Client Express App Disk
| | |
|--- POST /upload --------->| |
| (file data) | |
| | |
| Multer receives |
| the file |
| | |
| |--- Save to disk -|
| | |
| Generate filename |
| and return URL |
| | |
|<-- 200 OK (filename) ------| |
| (url) | |
| | |
|--- GET /uploads/file.jpg ->| |
| |--- Read file ----|
| | |
|<-- File contents ---------| |
| | |
The file goes to disk, then gets served back to the user.
Organize Uploaded Files
Simple (All in One Folder)
uploads/
├── profile-1.jpg
├── document-1.pdf
└── avatar-2.png
Works fine while you're learning.
Better (Organized by Type)
uploads/
├── profiles/
│ ├── user-1-pic.jpg
│ └── user-2-pic.jpg
├── documents/
│ ├── invoice-1.pdf
│ └── contract-1.pdf
└── avatars/
└── user-1-avatar.png
Easier to manage as your app grows.
Create Folders on Startup
const fs = require("fs");
const path = require("path");
function ensureFolders() {
const folders = [
"uploads",
"uploads/profiles",
"uploads/documents",
"uploads/avatars"
];
folders.forEach(folder => {
if (!fs.existsSync(folder)) {
fs.mkdirSync(folder, { recursive: true });
}
});
}
ensureFolders();
Save to Different Folders
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// Save images to profiles, documents to documents folder, etc.
if (file.mimetype.startsWith("image/")) {
cb(null, "uploads/profiles/");
} else if (file.mimetype === "application/pdf") {
cb(null, "uploads/documents/");
} else {
cb(null, "uploads/");
}
},
filename: (req, file, cb) => {
const uniqueName = Date.now() + "-" + Math.random().toString(36).substring(7);
cb(null, uniqueName);
}
});
Accessing Uploaded Files via URL
Once a file is saved, you need to make it accessible.
Serve from a Static Folder
app.use(express.static("uploads"));
// Now accessible at:
// http://localhost:3000/profile-pic.jpg
// http://localhost:3000/documents/resume.pdf
Custom Download Endpoint
app.get("/download/:filename", (req, res) => {
const filename = req.params.filename;
const filepath = path.join(__dirname, "uploads", filename);
if (!fs.existsSync(filepath)) {
return res.status(404).json({ error: "Not found" });
}
res.download(filepath);
});
Store References in Database
Instead of storing just the filename, save the full URL:
app.post("/upload-avatar", upload.single("avatar"), (req, res) => {
const userId = req.body.userId;
const fileUrl = `http://localhost:3000/${req.file.filename}`;
// Save to database
db.run(
"UPDATE users SET avatar = ? WHERE id = ?",
[fileUrl, userId],
(err) => {
if (err) {
return res.status(500).json({ error: "Database error" });
}
res.json({ url: fileUrl });
}
);
});
Security: Don't Let Uploads Break Your App
Files can be dangerous. Users might upload malicious files, or exploit your upload system. Here's what to do about it.
1. Validate File Type
Only accept certain file types:
const upload = multer({
dest: "uploads/",
fileFilter: (req, file, cb) => {
if (!file.mimetype.startsWith("image/")) {
return cb(new Error("Only images allowed"));
}
cb(null, true);
}
});
2. Limit File Size
Prevent users from uploading huge files:
const upload = multer({
dest: "uploads/",
limits: {
fileSize: 5 * 1024 * 1024 // 5MB max
}
});
3. Rename Files Safely
Never trust the original filename:
const storage = multer.diskStorage({
destination: "uploads/",
filename: (req, file, cb) => {
// Use a safe name
const uniqueName = Date.now() + "-" + Math.random().toString(36).substring(7);
cb(null, uniqueName);
}
});
4. Don't Serve Uploads from Code Folders
If uploads end up in your code folder, uploaded code might execute:
// WRONG
app.use(express.static("public")); // If uploads go here
// RIGHT
app.use(express.static("uploads")); // Separate folder
5. Control Access
Only let authorized users download certain files:
app.get("/user/:userId/avatar", (req, res) => {
if (req.user.id !== userId) {
return res.status(403).json({ error: "Not authorized" });
}
const filepath = path.join("uploads", `user-${userId}-avatar.jpg`);
res.sendFile(filepath);
});
6. Combined Security Example
const upload = multer({
dest: "uploads/",
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const allowed = ["image/jpeg", "image/png", "application/pdf"];
if (!allowed.includes(file.mimetype)) {
return cb(new Error("Invalid file type"));
}
cb(null, true);
}
});
Practice Assignment
1. Create a simple upload endpoint:
const express = require("express");
const multer = require("multer");
const app = express();
const upload = multer({ dest: "uploads/" });
app.post("/upload", upload.single("file"), (req, res) => {
// Send back file info
res.json({
filename: req.file.filename,
mimetype: req.file.mimetype,
size: req.file.size
});
});
app.use(express.static("uploads"));
app.listen(3000);
2. Add file validation:
// Modify the fileFilter to only allow images
const upload = multer({
dest: "uploads/",
fileFilter: (req, file, cb) => {
// Only images
}
});
3. Organize uploads by folder:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// Save images to uploads/images
// Save documents to uploads/documents
},
filename: (req, file, cb) => {
// Rename files safely
}
});
4. Create a file management endpoint:
// GET /files - list all uploaded files
// GET /download/:filename - download a file
// DELETE /files/:filename - delete a file (with auth)
Quick Recap
Files go to your server disk (local) or cloud service (external).
Local storage is simple but doesn't scale. Use for development.
Cloud storage scales automatically. Use for production.
Use
express.static()to serve files from a folder.Use
multermiddleware to receive and save files.Always rename files. Never trust user-provided filenames.
Limit file size to prevent disk filling up.
Validate file types before saving.
Store uploads in a separate folder, not in your code folder.
Control access: only let authorized users download certain files.
For production, use cloud storage (easier backups and scaling).
Get uploads right and they're straightforward. Get them wrong and you create security holes and scalability problems.
Happy coding! 🚀
If you enjoyed this article, check out my other blogs on this profile. 🔗 Connect with me: LinkedIn | GitHub | X (Twitter)




