File Uploads

How a server receives a file sent from a form or client, at a conceptual level — and what happens to it once it arrives.

What is it?

Most data a backend receives is simple text — JSON objects with strings and numbers. But sometimes a client needs to send an actual file: a profile picture, a PDF, a spreadsheet. Files are binary data, often large, and usually accompanied by other regular form fields (like a caption or a category) in the same request — which raw JSON isn't well suited to carrying alongside binary content.

To handle this, browsers and servers use a request format called multipart form data, which packages one or more files together with regular fields into a single request, each part clearly separated and labeled. On the server, a small library (like multer in the Node.js world) unpacks that multipart request, saving each file somewhere (disk, memory, or straight to cloud storage) and making its details available to your route handler.

Explain like I'm 10

A multipart form-data request is like a padded envelope containing several separately wrapped items — a letter (a text field), a photo (a file), and a receipt (another field) — each clearly labeled, so the person opening it (the server) can tell exactly what each piece is and handle it appropriately, rather than receiving one big unlabeled blob.

Examples

Accepting a single file upload with multer

const multer = require("multer");
const upload = multer({ dest: "uploads/" });

app.post("/profile-picture", upload.single("avatar"), (req, res) => {
  console.log(req.file);   // { filename, path, size, mimetype, ... }
  console.log(req.body);   // any other regular form fields sent alongside it
  res.json({ url: `/uploads/${req.file.filename}` });
});

multer runs as middleware, intercepting the multipart request, saving the uploaded file to disk, and attaching its details to req.file before the route handler ever runs.

Validating an upload before accepting it

const upload = multer({
  dest: "uploads/",
  limits: { fileSize: 2 * 1024 * 1024 }, // 2 MB max
  fileFilter: (req, file, cb) => {
    const allowed = ["image/png", "image/jpeg"];
    cb(null, allowed.includes(file.mimetype));
  },
});

Restricting file size and type at the upload layer prevents huge or unexpected files from ever reaching your application logic or storage.

How it works

When a form is submitted with a file, the browser encodes the request body as multipart/form-data instead of plain JSON: the body is split into distinct sections, each with its own small header describing whether it's a regular field or a file, and — for files — its original name and content type. On the server, an upload-handling library reads that multipart body, extracts each file's raw bytes, writes them somewhere (a temp folder, then often onward to permanent or cloud storage), and populates req.file (or req.files) with metadata your route can use, while regular fields land in req.body as usual.

Why does it exist?

Plain JSON bodies are text-based and not designed to efficiently carry large binary data alongside other fields. Multipart form data exists as a standard way to bundle files and regular form fields together in one request, and libraries like multer exist so that every project doesn't need to hand-write multipart parsing from scratch.

When to use it

Reach for multipart file uploads whenever a user needs to send an actual file — images, documents, videos — rather than just structured text data.

When not to use it

If a client only needs to reference an already-hosted file (a URL to an image already uploaded elsewhere) or send small amounts of encoded binary data, plain JSON with a base64-encoded string can be simpler — though it's less efficient for large files.

Common mistakes

  • Not limiting file size or type, allowing huge or unexpected files to exhaust server disk space or memory.

  • Storing uploaded files directly inside the app's own codebase directory instead of separate, dedicated storage.

  • Trusting the client-reported file extension or MIME type as proof of what the file actually contains.

Practice exercises

  1. Easy:

    Set up a route that accepts a single file upload under the field name 'document' and returns its saved filename.

  2. Medium:

    Add a file size limit of 5MB and reject any upload exceeding it with a clear error message.

  3. Hard:

    Explain why trusting a file's declared MIME type alone isn't sufficient to guarantee it's actually a safe image file, and describe one additional check you could add.

Interview questions

What is multipart form data and why is it used for file uploads?

A request format that bundles files and regular form fields together, each clearly separated and labeled, since plain JSON isn't well suited to carrying large binary content alongside text fields.

What role does a library like multer play in handling uploads?

It parses the incoming multipart request, extracts uploaded files, saves them (to disk, memory, or onward to cloud storage), and exposes their metadata to the route handler via req.file or req.files.

Why should you limit file size and validate file type on the server, not just the frontend?

Because a request can bypass the frontend entirely, so limits enforced only there provide no real protection against oversized or malicious uploads.

What's the difference between multer's `memoryStorage` and `diskStorage`?

memoryStorage buffers the entire file in RAM, via req.file.buffer, which is simple but risky for large files or high concurrency since it can exhaust server memory; diskStorage streams the file to a temporary location on disk instead, using less memory per upload but requiring cleanup of those temp files afterward.

Why is streaming an uploaded file directly to storage preferable to fully loading it into memory first, for large files?

Streaming processes the file in small chunks as they arrive, keeping memory usage roughly constant regardless of file size; loading an entire large file into memory first means memory usage scales with file size, which can exhaust the server under concurrent large uploads.

What's the security risk of trusting a file's client-reported MIME type or extension?

Both are just metadata the client supplies and can be forged — a file named photo.jpg with a claimed image/jpeg type could actually contain a completely different format; the real content should be verified server-side, e.g. by inspecting the file's actual byte signature, not just trusting the label.

Why store uploaded files outside the application's own served codebase directory, or in dedicated object storage like S3?

Storing user-uploaded content inside the codebase risks it being served or executed as part of the app itself, complicates deployments that redeploy the whole codebase, and doesn't scale well across multiple server instances the way dedicated, shared storage does.

What happens if `upload.single(fieldName)` receives a request where the client used a different field name than expected?

req.file isn't populated at all — the middleware only looks for a file under that exact field name, so the route needs to check whether req.file actually exists rather than assuming it always will.

Why should an uploaded file be given a new, generated filename on the server rather than keeping the client-supplied original filename?

The original filename is attacker-controlled and can contain path traversal sequences, unexpected characters, or collide with another user's file of the same name; generating a new unique name, like a UUID, server-side avoids both the security risk and accidental overwrites.

How does `upload.array(fieldName, maxCount)` differ from `upload.single(fieldName)`?

.single() expects exactly one file under that field name and populates req.file; .array() accepts multiple files under the same field name, up to maxCount, and populates req.files as an array instead.

Why might you validate an image upload's actual pixel dimensions or re-encode it server-side, beyond checking its MIME type and size?

A file can pass MIME-type and size checks while still being a maliciously crafted image, exploiting a bug in an image-processing library, or one with small file size but enormous decoded dimensions; re-encoding through a trusted image library both normalizes the format and neutralizes most such attacks.

What HTTP status code would you typically return if an upload exceeds the configured size limit?

413 Payload Too Large — multer's fileSize limit surfaces as an error that the route or centralized error middleware should catch and turn into this response, rather than letting it manifest as a generic crash.

Why is `multipart/form-data` less efficient than raw binary transfer, and when is that trade-off still worth it?

Multipart encoding adds boundary markers and headers around each part, which can inflate the payload somewhat versus raw bytes; it's still worth it whenever a file needs to travel alongside other form fields in a single request, which a raw binary body alone can't represent.

Why check a file's extension or magic-byte signature, in addition to its declared MIME type?

A MIME type is a single header value that's easy to spoof; an extension or byte-signature check adds another independent check an attacker also has to get right, making it harder to slip a disguised file past validation with one forged value.

Why does an upload route usually need both a `fileFilter` and a size limit?

A size limit alone still allows any file type through as long as it's small enough — fileFilter runs during multipart parsing and rejects unwanted MIME types before the file is even written to storage, so the two checks guard against different problems and both are needed.

What happens to `req.body` fields sent alongside a file in the same multipart request?

They're still parsed and populated onto req.body as usual — multer separates the file part(s) into req.file/req.files while leaving ordinary text fields on req.body, so both are available together in the route handler.