# Storing Uploaded Files and Serving Them in Express

As we have seen in previous article how to upload file using express and multer. Uploading a file is just the first half of the story. The other half where it lives and how you retrieve it.

## Where Uploaded Files Actually Live

When Multer processes a file, it writes it to a location on your server's filesystem. Exactly where depends on how you configured your storage. With the simple `dest` option, all files go into one flat folder. With `diskStorage`, you control every part of the path.

By default, that folder is relative to wherever your `node` process is started usually the project root. So `dest: 'uploads/'` means the files end up at `your-project/uploads/`. They live on disk like any other files, readable by any process with access to that directory.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/55f12657-331b-4094-b623-64f9019cb452.png align="center")

> Use subfolders within `uploads/` to separate different file types — avatars, documents, product images. It keeps the directory manageable and makes it easier to apply different policies (expiry, access control) per category.

## Local Storage vs. External Storage

When you first build file upload functionality, storing files on the same server your application runs on **local disk storage** is the natural choice. It's simple, fast, and requires no third-party services. For development and low-traffic applications, it works perfectly well.

At scale, though, local disk has a few fundamental problems: disk space is finite, files don't survive server replacements or container restarts, and if you run multiple instances of your server behind a load balancer, only the instance that received the upload has the file. External storage services like S3, Cloudinary, or Google Cloud Storage exists to solve these problems.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/87e9be09-2bd1-4d82-969d-32941c2d9d84.png align="center")

This article focuses on local disk — the right place to start. External storage integrations (AWS S3, etc.) follow the same conceptual model but involve additional SDK configuration. Once you understand local storage, the leap to cloud storage is mostly configuration, not new concepts.

## Serving Static Files in Express

Express ships with a built-in middleware called `express.static()`. When you mount it, Express looks in the specified folder for a matching file whenever a GET request comes in. If a file is found, Express serves it directly without touching any of your route handlers. If it isn't found, the request falls through to whatever comes next in your middleware chain.

```javascript
const express = require('express');
const path    = require('path');
const app     = express();

// Serve files from the uploads/ directory
// under the URL prefix /uploads
app.use(
  '/uploads',
  express.static(path.join(__dirname, 'uploads'))
);

// Now a file at: uploads/avatars/user-001.jpg
// Is served at:  GET /uploads/avatars/user-001.jpg
```

Using `path.join(__dirname, 'uploads')` instead of a relative string is worth noting. Relative paths are resolved from wherever the `node` process was launched, which can cause confusing "file not found" errors if you start your server from a different directory. `__dirname` is always the directory of the current file reliable regardless of where you invoke node.

> `express.static()` *is essentially a tiny file server baked into your Express app. It handles caching headers, ETags, range requests, and content-type detection things you'd spend hours implementing yourself.*

### Controlling the URL prefix

The first argument to `app.use()` sets the URL prefix. You can use any path you like it doesn't have to match the folder name. A user never needs to know your server's folder structure:

```javascript
// Folder: uploads/
// Public URL prefix: /media
app.use('/media', express.static('uploads'));

// File on disk: uploads/photo.jpg
// Accessed via: GET /media/photo.jpg  
```

## Accessing Uploaded Files via URL

The most common pattern is to return the file's URL immediately after upload, so the client can display or link to it right away. Your route handler should construct the URL from the saved filename:

```javascript
app.post('/upload/avatar', upload.single('avatar'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file received' });
  }

  // Build a public-facing URL from the saved filename
  const fileUrl = `${req.protocol}://${req.get('host')}/uploads/${req.file.filename}`;

  // Typically you'd save fileUrl to a database here
  res.status(201).json({
    message: 'Upload successful',
    url:     fileUrl,
  });
});
```

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/36541420-34d0-41a6-a4d4-8e83fadb8dfc.png align="center")

## Security Considerations for Uploads

File uploads are one of the most common attack vectors in web applications. A form that accepts files is essentially an invitation for users to send whatever they want to your server. The defaults are not safe you have to explicitly lock things down.

### The non-negotiables

*   **Always set a file size limit.** Without `limits.fileSize`, a user can upload a multi-gigabyte file and crash your server.
    
*   **Validate the file type server-side.** Checking `file.mimetype` is a minimum. For sensitive use cases, inspect magic bytes with a library like `file-type`.
    
*   **Never use the original filename.** A user could send `../../../etc/passwd` as a filename. Always generate your own — Multer's random hash or your own UUID.
    
*   **Store uploads outside the web root.** If your uploads folder is outside `public/`, it can't be served accidentally. Only expose files you explicitly serve via static middleware.
    
*   **Never execute uploaded files.** Even if you're expecting a script file, never run it. Uploaded files should be treated as data — read, stored, downloaded. Never executed.
    
*   **Consider image resizing/re-encoding.** A malicious PNG can embed harmful data in metadata or exploit decoder vulnerabilities. Re-encoding images with a library like `sharp` strips unknown data.
    

### Error handling for rejected uploads

When Multer rejects a file due to file size, bad type, or too many files — it passes an error to the next middleware. You need to catch this explicitly, otherwise Express will respond with a generic 500:

```javascript
app.post('/upload', (req, res, next) => {
  upload.single('image')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      // Multer-specific errors (LIMIT_FILE_SIZE, etc.)
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(413).json({ error: 'File too large. Max 5 MB.' });
      }
      return res.status(400).json({ error: err.message });
    }
    if (err) {
      // fileFilter or other custom errors
      return res.status(400).json({ error: err.message });
    }

    // No error — proceed normally
    handleUpload(req, res);
  });
});

function handleUpload(req, res) {
  if (!req.file) return res.status(400).json({ error: 'No file' });
  res.json({ url: `/uploads/${req.file.filename}` });
}
```

## Complete Example: Upload, Store, Serve

Here is a fully working minimal server that combines storage configuration, static file serving, file type filtering, size limits, and proper error handling:

```javascript
const express = require('express');
const multer  = require('multer');
const path    = require('path');
const fs      = require('fs');

const app        = express();
const UPLOAD_DIR = path.join(__dirname, 'uploads');

// Ensure the uploads folder exists
fs.mkdirSync(UPLOAD_DIR, { recursive: true });

// Storage 
const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, UPLOAD_DIR),
  filename:    (req, file, cb) => {
    const ext    = path.extname(file.originalname).toLowerCase();
    const unique = `${Date.now()}-` + Math.random().toString(36).slice(2);
    cb(null, unique + ext);
  },
});

// Multer instance 
const upload = multer({
  storage,
  limits:     { fileSize: 5_000_000 },
  fileFilter: (req, file, cb) => {
    const ok = ['image/jpeg', 'image/png', 'image/webp'];
    cb(ok.includes(file.mimetype) ? null : new Error('Images only'), ok.includes(file.mimetype));
  },
});

// Static serving 
app.use('/uploads', express.static(UPLOAD_DIR));

// Upload route (with error handling) 
app.post('/upload', (req, res) => {
  upload.single('image')(req, res, (err) => {
    if (err) {
      const status = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
      return res.status(status).json({ error: err.message });
    }
    if (!req.file) {
      return res.status(400).json({ error: 'No file provided' });
    }

    const fileUrl = `/uploads/${req.file.filename}`;
    res.status(201).json({ url: fileUrl });
  });
});

app.listen(3000, () => console.log('Server on :3000'));
```
