Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 39

File Upload with Multer

Recommended resources

Introduction

In the previous lesson, you successfully set up a basic file upload endpoint using Multer's simplest configuration. While functional, that implementation has a couple of drawbacks for a production environment:

  • Random Filenames: It saves files with random names and no file extensions, making them difficult to manage and serve.
  • No Validation: It accepts any type of file, which can be a security risk and lead to application errors.

This lesson is about refining our POST route to make it more robust and production-ready. We will replace the basic configuration with a more advanced setup that gives us full control over how files are stored and what types of files are accepted.

By the end of this lesson, you will have implemented a POST route that uses Multer's diskStorage engine to control filenames and a fileFilter function to validate file types.


1. Gaining Control with diskStorage and fileFilter

To address the shortcomings of the basic setup, Multer provides two powerful configuration options:

  1. storage: This option lets you define a custom storage engine. We will use multer.diskStorage to control both the destination directory and the filename of uploaded files, including their extensions.
  2. fileFilter: This option is a function that allows you to programmatically decide whether to accept or reject an incoming file based on its properties, such as its MIME type (image/png, application/pdf, etc.).

The following video provides an excellent, practical demonstration of implementing both diskStorage and fileFilter. We'll watch it in two parts.


2. Implementing Custom Storage with diskStorage

First, let's see how to use diskStorage to manage where files are saved and how they are named. This ensures your uploaded files retain their correct extensions and have unique names to prevent conflicts.

Uploading files with Node.js + Express using Multer

This segment from a tutorial by manfra.io demonstrates how to configure Multer's diskStorage engine. It's a clear, step-by-step guide to taking control of your file uploads.

Watch the video from 06:43 to 10:54. Focus on these key concepts: Creating the storage object using multer.diskStorage(). The destination function: How it receives the req, file, and a callback (cb), and how it's used to specify the upload folder. Notice the use of the fs module to check if the directory exists. The filename function: How it uses Date.now() and the path module to construct a unique filename while preserving the original file's extension (file.originalname).


3. Validating File Types with fileFilter

Now that we control the filename, the next crucial step is to ensure that users can only upload file types that our application expects. A fileFilter is the perfect tool for this.

Uploading files with Node.js + Express using Multer

Continuing with the same tutorial, this next part shows how to implement a fileFilter to accept or reject files based on their type. Pay close attention to how a custom error response is sent back to the client for invalid files.

Watch the video from 12:02 to 18:05. Pay attention to: The fileFilter function signature: (req, file, cb). Checking the MIME type: How the code inspects file.mimetype to see if it's 'image/png' or 'image/jpeg'. Using the callback: cb(null, true) to accept a file and cb(null, false) to reject it. Handling Rejection: The clever technique of attaching an error message to the req object (req.errorMessage = ...) within the filter, which is then used in the main route handler to send a specific 422 status and JSON error message.


4. Your Turn: Implement the Enhanced Upload Route

It's time to apply these patterns to your project. You will replace the simple Multer configuration in your routes/uploadRoutes.js file with this more robust setup.

Action Steps:

  1. Open your routes/uploadRoutes.js file.
  2. Import the built-in path and fs modules at the top of the file, as you'll need them for the new configuration.
  3. Replace your existing Multer configuration and route with the code below.
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const router = express.Router();

const uploadsDir = './uploads';

// Configure Multer storage engine for full control over file storage
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    // This function specifies the destination directory for uploads.
    // It's good practice to ensure the directory exists.
    if (!fs.existsSync(uploadsDir)) {
      fs.mkdirSync(uploadsDir);
    }
    cb(null, uploadsDir);
  },
  filename: function (req, file, cb) {
    // This function defines the filename.
    // We create a unique filename to avoid overwrites and conflicts.
    // `path.extname` extracts the file extension from the original filename.
    cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname));
  }
});

// Define a file filter to allow only specific image types
const fileFilter = (req, file, cb) => {
  const allowedMimeTypes = ['image/jpeg', 'image/png'];
  if (allowedMimeTypes.includes(file.mimetype)) {
    cb(null, true); // Accept file
  } else {
    // Reject file and create a custom error to be handled in the route
    req.fileValidationError = 'Invalid file type. Only JPEG and PNG are allowed.';
    cb(null, false); // Silently reject the file
  }
};

const upload = multer({ 
  storage: storage,
  fileFilter: fileFilter,
  limits: { fileSize: 1024 * 1024 * 5 } // Optional: 5MB file size limit
});

// Update the POST route to use the new advanced configuration
router.post('/', upload.single('productImage'), (req, res) => {
  // Check for the custom validation error from our fileFilter
  if (req.fileValidationError) {
    return res.status(400).json({ error: req.fileValidationError });
  }

  // NOTE: We are not yet handling the case where the user submits the form
  // without a file. This will be the focus of the next lesson.
  if (!req.file) {
      // This is a placeholder for the next lesson's logic.
      // For now, an invalid file type will trigger this, but we'll soon differentiate.
      return res.status(400).json({ error: 'File upload failed. Please ensure file is a valid type.' });
  }
  
  // If we get here, the file was uploaded and passed the filter.
  res.status(200).json({
    message: 'File uploaded successfully!',
    fileInfo: req.file,
  });
});

module.exports = router;

Test Your Implementation

After updating your code, restart your server and use Postman to test the endpoint (POST /api/upload) with the field name productImage.

  1. Test a valid file: Upload a .jpg or .png file. You should get a 200 OK status and a JSON response with the file's details. Check your uploads folder to see the file saved with its new, unique name and correct extension.
  2. Test an invalid file: Try uploading a .txt, .pdf, or any other non-image file. You should now receive a 400 Bad Request status and the JSON error message: {"error": "Invalid file type. Only JPEG and PNG are allowed."}.

Conclusion

Fantastic! You have now implemented a production-ready file upload route. By using diskStorage and fileFilter, you've gained precise control over the upload process, making your API more secure and predictable.

Key Takeaways:

  • multer.diskStorage is used to define custom logic for the file's destination and filename.
  • The filename function within diskStorage is crucial for creating unique names and preserving file extensions using path.extname().
  • fileFilter is a function that validates incoming files, typically by checking file.mimetype.
  • You can pass custom error information from the fileFilter to your route handler by attaching it to the req object.

Next Up:

Our route is much more robust, but there's one more validation check we need to add: what happens if the user submits the request without attaching a file at all? In the next lesson, we will implement this final validation check and then write an automated integration test for our file upload endpoint using Supertest's .attach() method.