Skip to main content

Command Palette

Search for a command to run...

Storing Uploaded Files and Serving Them in Express

Published
6 min read
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:

  1. Multer intercepts the request

  2. Saves the file to the uploads/ folder with a unique name

  3. Adds file information to req.file

  4. Your 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.