Handling file uploads

Accept file uploads in an Express app with multer — save to disk, cap the size, reject the wrong types — plus the honest truth about disk on a Falix server and where big files really belong.

Letting users upload a file — an avatar, a screenshot, a document — is a common feature and an easy one to get wrong. The two things people forget are limits (nothing stops a 4 GB upload by default) and where the file actually goes. This guide handles both with multer, the standard Express upload middleware, and is honest about disk on a Falix server.

At a glance
You need A server running the Node.js application
Background Build an Express website + API
Plan Any plan
Time Twenty-five minutes

Why you need a library for this

Uploads don't arrive as JSON — they come as multipart/form-data, a streamed format express.json() can't parse. multer reads that stream, writes each file where you tell it, and hands your route a tidy req.file describing what landed. Install express and multer from the Packages page (search each, install, restart).

Save to disk, with limits from the start

Here's a complete index.js. It writes uploads into an uploads/ folder, gives each a collision-proof name, caps the size at 2 MB, and only accepts images:

const express = require('express');
const multer = require('multer');
const fs = require('node:fs');

const app = express();
fs.mkdirSync('uploads', { recursive: true }); // ensure the folder exists

const storage = multer.diskStorage({
  destination: 'uploads/',
  filename: (req, file, cb) => {
    // Never trust the original name — sanitize it and add a timestamp.
    const safe = Date.now() + '-' + file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
    cb(null, safe);
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 2 * 1024 * 1024 }, // 2 MB — set this before you need it
  fileFilter: (req, file, cb) => {
    if (['image/png', 'image/jpeg', 'image/gif'].includes(file.mimetype)) cb(null, true);
    else cb(new Error('Only PNG, JPEG, and GIF images are allowed'));
  },
});

// upload.single('file') = one file, from a form field named "file"
app.post('/upload', upload.single('file'), (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
  res.status(201).json({ stored: req.file.filename, size: req.file.size });
});

// Turn multer's errors into clean JSON instead of a stack trace.
app.use((err, req, res, next) => {
  if (err.code === 'LIMIT_FILE_SIZE') {
    return res.status(413).json({ error: 'File too large (max 2 MB)' });
  }
  return res.status(400).json({ error: err.message });
});

const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));

Three deliberate choices carry this file:

  • The size limit is not optional. limits.fileSize makes multer stop reading and throw LIMIT_FILE_SIZE the moment a file crosses the line — before it fills your disk. Pick a number that fits your use and set it now.
  • The filename is sanitized. A user could name a file ../../index.js. Stripping everything but letters, digits, dots and dashes and prefixing a timestamp kills both path tricks and name collisions.
  • The type is checked. fileFilter rejects anything that isn't an image, so /upload can't become a place to stash arbitrary files.

Verify it works

Start the server and upload from any terminal with curl -F (the flag that sends multipart/form-data):

# a valid image -> 201 Created
curl -F "[email protected];type=image/png" http://YOUR_ADDRESS:PORT/upload
# no file -> 400
curl -X POST http://YOUR_ADDRESS:PORT/upload
# a .txt -> 400, rejected by fileFilter
curl -F "[email protected];type=text/plain" http://YOUR_ADDRESS:PORT/upload
# a 3 MB file -> 413, rejected by the size limit
curl -F "[email protected];type=image/png" http://YOUR_ADDRESS:PORT/upload

A successful upload returns {"stored":"...","size":...} and the file appears in uploads/ in your File Manager. That's the whole loop.

Serving the files back

Once files are on disk, hand them back to browsers with express.static, pointed at the same folder:

app.use('/uploads', express.static('uploads'));
// an uploaded photo is now at http://YOUR_ADDRESS:PORT/uploads/<filename>

⚠️ Heads up: express.static serves whatever is in the folder to anyone who knows the URL. If uploads should be private, don't serve the folder directly — gate a download route behind a login (Sessions, JWT & passwords) instead.

The disk reality on Falix

This is the part tutorials skip. Files you save go to your server's disk under /home/container, and that has consequences worth knowing before you build on it:

Question The honest answer
Do uploads survive a restart? Yes — they're on disk, not in memory.
Do they survive a reinstall / switching the application? No. That wipes the server's files. Back up anything you can't lose.
How much space is there? Free plans share a modest disk; premium is effectively unlimited. Large media fills a small disk fast.
Are they backed up? Only if you back them up — see the Backups page (free plans back up to your own Google Drive).
Is there a CDN in front? No. Every download comes straight from your one server on your one port.

The takeaway: the local disk is fine for small, replaceable files — avatars, attachments a database also references. For lots of large media, or anything you must never lose, keep the file somewhere durable (object storage like S3-compatible buckets) and store only its URL in your database. Multer also has a memoryStorage mode that keeps the upload in RAM as a buffer — handy when you immediately forward it elsewhere, but it competes with your app for the free plan's 2.5 GB, so cap the size hard.

Everything past this — multiple files (upload.array), mixed fields, custom storage engines — is standard multer. The official docs live at github.com/expressjs/multer.

Troubleshooting

  • req.file is undefined — the form field name doesn't match upload.single('file'), or the request wasn't multipart/form-data. With curl, use -F; in a browser form, set enctype="multipart/form-data" and matching field names.
  • LIMIT_FILE_SIZE — the upload exceeded your limits.fileSize. That's the guard working; raise the limit only if you truly need to.
  • ENOENT: no such file or directory 'uploads/' — the destination folder didn't exist. The fs.mkdirSync('uploads', { recursive: true }) line above prevents this.
  • Disk keeps filling — old uploads never get deleted on their own. Clean them up, or move files off the server to durable storage. Watch usage on the Console status bar.

Next steps

Was this guide helpful?