All Case StudiesSahil Atahar
5 min read #NodeJS #File-Upload

Why i stopped using Multer for Everything

What started as a simple Multer setup broke down as the app scaled. The fix was separating file upload from data submission using signed URLs to cloud storage; the server just tracks status.

Why i stopped using Multer for Everything

Overview

I started out uploading files with multer as everyone recommends, but as my application grew- more fields, more nested objects, bigger files that approach broke down in three specific ways. The fix was to stop treating file upload and data submission as one operation: split them apart, upload files directly to cloud storage using signed URLs, and let the server only track upload status instead of handling the bytes itself. Below is the full breakdown of what went wrong, how I found it, what I did, how the new flow works, and what I'd tell someone hitting the same wall.

What Was the Issue

When I started learning Node.js, everyone said the same thing: "Use multer, set your fields, and you're done." And multer is genuinely good, with a lot of options. But as my application grew and the number of fields in my documents increased, this approach started to fall apart.

How I Found the Problem

It showed up in three different ways as my projects scaled:

1. Everything became a string. I was sending data and files together as FormData. That meant nested objects and every data type got flattened into strings. I had to manually parse and re-cast everything on the server. With a handful of fields, this is fine, but once I had large collections with many nested objects, it turned into a lot of extra handling code and extra time.

2. The server became the bottleneck. I was uploading files and storing them directly on the server first. If I forgot to clean something up, or traffic increased, the server would run into space issues. Files are the one thing that just keeps eating disk space silently.

3. Large files simply didn't work. If I ever needed to upload a video file in GBs, I couldn't upload it to the server first and then push it to S3 or another platform. The server would have to hold the entire file in the meantime, which doesn't scale.

What I Did

The main fix was simple in concept: separate file upload from data update.

Instead of sending files and data together, I split them into two independent flows: one API for the actual data, and a separate upload flow purely for files. That same upload flow could still push to the server first and forward to S3, but because of issue #3, I picked a different approach for the long run: upload files directly to the cloud, and never let them touch the server at all.

The Main Cause

The root problem was that I was treating file upload and data submission as one operation, when they have completely different constraints: one is about validating structured data, the other is about moving large binary blobs efficiently. Bundling them together meant every constraint of the "worst" one (large files) applied to the whole request.

If you haven't seen this explained well, this short video breaks down the exact mistake I was making and why the server shouldn't touch the file bytes at all: Why your upload server should never touch the file bytes

How It Works

  1. The client requests a file upload and sends the details: size limit, file type, name, and any relevant IDs.
  2. The server validates those details and creates a file record with status pending.
  3. The server generates a signed URL scoped to that file record and returns it to the client.
  4. The client uploads the file directly to that signed URL (S3 or any other cloud storage) — the server never sees the bytes.
  5. Once the upload finishes, the client sends the file ID back to the server.
  6. The server verifies the file exists at that path and updates the record's status to uploaded.

Cleanup

Not every file that gets a pending record actually gets uploaded; the client might create the record and then abandon the flow. To handle that, a cron job runs periodically to identify old pending records older than a certain age and deletes both the database record and the corresponding orphaned file on S3 (or whichever platform is used), so nothing lingers and racks up storage costs.

My Takeaways

  • Multer isn't bad; I still use it when a file needs to be attached and sent straight to email. For that one-shot use case, it's simple and does exactly what's needed.
  • The mistake wasn't the tool; it was using one tool for every shape of problem. Structured data and large binary uploads have different needs, and forcing them through the same request punishes both.
  • Decoupling file upload from data submission and letting the client talk to storage directly via signed URLs, solved all three problems at once: no more string-flattened nested data, no server disk pressure, and no size ceiling on uploads.
  • A cleanup cron job isn't optional in this pattern; pending records without a cleanup step will quietly pile up.

Happy coding 🚀.