Skip to main content

Command Palette

Search for a command to run...

Storing Uploaded Files and Serving Them in Express

Updated
8 min read
Storing Uploaded Files and Serving Them in Express
H
CS undergrad | Tech enthusiast | Focusing on Web Dev • DSA • ML | Building skills for real-world impact

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 multer middleware 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)