Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

File uploads

Files from a form arrive in a special multipart/form-data format, which Express's built-in middleware does not parse. The multer package does it: it saves the file to disk and puts information about it into req.file. Let us add a cover image to blog posts.

Installation

npm install multer

Setup

Create the file src/middleware/upload.js. It describes where and under which name files are saved:

// src/middleware/upload.js
import path from 'node:path';
import crypto from 'node:crypto';
import multer from 'multer';
 
const storage = multer.diskStorage({
  destination: 'public/uploads',
  filename(req, file, callback) {
    const extension = path.extname(file.originalname).toLowerCase();
    callback(null, `${crypto.randomUUID()}${extension}`);
  },
});
 
export const upload = multer({
  storage,
  limits: { fileSize: 2 * 1024 * 1024 },
  fileFilter(req, file, callback) {
    const allowed = ['image/jpeg', 'image/png', 'image/webp'];
    callback(null, allowed.includes(file.mimetype));
  },
});

Files go to public/uploads/ under random names, the size is limited to 2 MB, and only JPEG, PNG and WebP images are accepted. Create the public/uploads folder beforehand.

The form

The form in views/posts/new.ejs needs the enctype attribute and a field of type file:

<form method="post" action="/posts" enctype="multipart/form-data">
  <input name="title">
  <textarea name="body"></textarea>
  <input type="file" name="cover" accept="image/*">
  <button>Publish</button>
</form>

Without enctype="multipart/form-data" the browser sends only the file name, not its contents.

The route

The upload.single('cover') middleware goes before the handler. Its argument is the name of the file field:

// src/routes/posts.js
import { upload } from '../middleware/upload.js';
 
router.post('/', upload.single('cover'), postsController.store);

The handler

Multer also parses the text fields of the form, so req.body works as before. The file information is in req.file:

// src/controllers/postsController.js
export function store(req, res) {
  const post = {
    id: posts.length + 1,
    title: req.body.title,
    body: req.body.body,
    cover: req.file ? `/uploads/${req.file.filename}` : null,
  };
  posts.push(post);
 
  res.redirect(`/posts/${post.id}`);
}

If no file was chosen or it did not pass the filter, req.file is undefined. On the post page the cover is shown with <img src="<%= post.cover %>" alt=""> inside an if (post.cover) condition.

req.file fieldContents
filenameThe name of the saved file
originalnameThe file name on the visitor's computer
mimetypeThe file type, for example image/png
sizeSize in bytes

A file that is too big

If a file exceeds the limit, multer passes an error with the code LIMIT_FILE_SIZE to the error handler. You can show the visitor a clear message:

// src/app.js
import multer from 'multer';
 
app.use((error, req, res, next) => {
  if (error instanceof multer.MulterError) {
    return res.status(400).send('The file is too big: 2 MB at most');
  }
  next(error);
});

This handler goes before the general error handler and passes all other errors on.

Common mistake

Do not save a file under the name the visitor sent. A name like ../../src/app.js, or one matching someone else's file, could overwrite important data. Generate the name on the server, as in the setup above, and if needed keep the original name separately in the post data.

Thanks for reading!

Donate to the site — we work hard for you.