Episode 3 — NodeJS MongoDB Backend Architecture / 3.7 — Handling Files with Express

3.7 — Handling Files with Express: Quick Revision

Episode 3 supplement -- print-friendly.

How to use

Skim -> drill weak spots in 3.7.a through 3.7.f -> 3.7-Exercise-Questions.md.


Why Multer?

  • express.json() handles application/json -- cannot handle files
  • express.urlencoded() handles application/x-www-form-urlencoded -- cannot handle files
  • Files require multipart/form-data -- only Multer (or similar) can parse it
  • HTML forms need enctype="multipart/form-data" for file inputs

Install and Basic Setup

npm install multer
const express = require('express');
const multer = require('multer');

const app = express();
const upload = multer({ dest: 'uploads/' });   // simplest config

app.use(express.json());                        // still needed for JSON routes

app.post('/upload', upload.single('avatar'), (req, res) => {
  console.log(req.file);   // file info
  console.log(req.body);   // text fields
  res.json({ file: req.file });
});

Upload Methods

MethodUsagereq.filereq.files
upload.single('name')One fileFile object--
upload.array('name', max)Multiple, same field--Array
upload.fields([...])Multiple, different fields--Object of arrays
upload.none()Text-only multipart----
upload.any()Any field (avoid in prod)--Array

Storage Engines

Disk Storage

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, 'uploads/'),
  filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname)
});
const upload = multer({ storage });
  • File saved to disk immediately
  • Access via req.file.path and req.file.filename
  • Good for: local processing, permanent storage on server

Memory Storage

const upload = multer({ storage: multer.memoryStorage() });
  • File held as Buffer in RAM
  • Access via req.file.buffer
  • Good for: cloud upload pipeline, magic bytes validation
  • Risk: memory exhaustion with large files

When to Use Which

CriteriaDiskMemory
File touches disk?YesNo
Available as Buffer?No (read manually)Yes (req.file.buffer)
Cloud upload?Upload from file pathStream buffer directly
Large files?SafeRisky (RAM)
Temp cleanup needed?YesNo

req.file Properties Table

PropertyDisk StorageMemory StorageDescription
fieldnameYesYesForm field name
originalnameYesYesOriginal filename from client
encodingYesYesFile encoding (e.g., 7bit)
mimetypeYesYesMIME type (e.g., image/jpeg)
destinationYes--Upload folder path
filenameYes--Generated filename on disk
pathYes--Full path to saved file
sizeYesYesFile size in bytes
buffer--YesFile data as Buffer

File Filter (Validation)

const imageFilter = (req, file, cb) => {
  if (file.mimetype.startsWith('image/')) {
    cb(null, true);                                      // accept
  } else {
    cb(new Error('Only images are allowed'), false);     // reject
  }
};

const upload = multer({
  storage: multer.memoryStorage(),
  fileFilter: imageFilter,
  limits: { fileSize: 5 * 1024 * 1024 }                 // 5 MB
});

Size Limits

limits: {
  fileSize: 5 * 1024 * 1024,   // 5 MB per file
  files: 10,                    // max 10 files per request
  fields: 20,                   // max 20 text fields
  fieldSize: 1024 * 1024        // max 1 MB per text field value
}

Error Handling

app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: 'File too large' });
    }
    return res.status(400).json({ error: err.message });
  }
  if (err) return res.status(400).json({ error: err.message });
  next();
});
Error CodeCause
LIMIT_FILE_SIZEFile exceeds limits.fileSize
LIMIT_FILE_COUNTToo many files
LIMIT_UNEXPECTED_FILEField name mismatch
LIMIT_FIELD_COUNTToo many text fields

Magic Bytes Validation

const fileType = require('file-type');

const result = await fileType.fromBuffer(req.file.buffer);
if (!result || !['image/jpeg', 'image/png'].includes(result.mime)) {
  return res.status(400).json({ error: 'Invalid file type' });
}
FormatMagic Bytes
JPEGFF D8 FF
PNG89 50 4E 47
PDF25 50 44 46
GIF47 49 46 38

Security Checklist

  • Set limits.fileSize -- never allow unlimited uploads
  • Set fileFilter -- whitelist allowed MIME types
  • Validate magic bytes -- do not trust client-sent MIME
  • Sanitize filenames -- strip ../, special characters, use UUIDs
  • Store files outside the web root -- prevent direct execution
  • Use cloud storage in production -- not local disk
  • Serve user files with Content-Disposition: attachment when possible
  • Delete old files when updating or removing resources
  • Never serve an uploads folder with express.static()

Cloud Upload Pattern (Cloudinary)

Configuration

const cloudinary = require('cloudinary').v2;
cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET
});

Upload Buffer to Cloudinary

function uploadToCloudinary(buffer, folder) {
  return new Promise((resolve, reject) => {
    cloudinary.uploader.upload_stream(
      { folder, resource_type: 'auto' },
      (error, result) => error ? reject(error) : resolve(result)
    ).end(buffer);
  });
}

// In route handler:
const result = await uploadToCloudinary(req.file.buffer, 'avatars');
// result.secure_url  -- the CDN URL
// result.public_id   -- needed for deletion

Delete from Cloudinary

await cloudinary.uploader.destroy(publicId);

URL Transformations

https://res.cloudinary.com/CLOUD/image/upload/w_200,h_200,c_fill,g_face,q_auto,f_auto/PUBLIC_ID.jpg
ParamEffect
w_200Width 200px
h_200Height 200px
c_fillCrop to fill dimensions
g_faceCenter on detected face
q_autoAuto quality
f_autoAuto format (WebP/JPEG)

Production Pipeline

Client (FormData) -> Multer (memory) -> Validate (MIME + magic bytes)
    -> Cloudinary (upload_stream) -> Save URL to MongoDB -> Return URL to client

Key rule: The database stores the URL, never the file. The server never permanently stores the file on disk.


One-Liners

  • Multer = middleware that parses multipart/form-data for file uploads.
  • Disk storage = files on filesystem. Memory storage = files as Buffers in RAM.
  • fileFilter = accept/reject files before storage. limits = cap size and count.
  • Magic bytes = first bytes of a file reveal its true type. More reliable than MIME.
  • Cloud storage = upload to Cloudinary/S3; store URL in DB; serve via CDN.

End of 3.7 quick revision.