The early web ran on plain text. You typed a username or a comment into a form, hit submit, and a short string traveled to the server over HTTP. That simple request-response cycle defined the infrastructure. When people started wanting to share photos, documents, and videos, engineers had to figure out how to move raw binary data through a system built entirely for readable text.

If you try to drop a JPEG into a JSON object, you hit a fundamental wall. JSON is a text protocol. It expects valid Unicode characters, quotes, braces, and properly escaped strings. A binary file is just a long sequence of bytes, many of which have no printable representation. Jam those bytes into a JSON string and the parser breaks, escape sequences corrupt the payload, and the whole message becomes unreadable on the other side.

Base64 encoding emerged as the obvious workaround. It re-maps binary data into a limited set of sixty-four printable ASCII characters. Every three bytes of binary become four characters of text. The payload is now legal JSON, which means it will survive a trip through any standard API. But the cost is immediate. That re-encoding inflates the file size by roughly thirty-three percent. A three megabyte image becomes four megabytes on the wire. Both the client and the server burn extra CPU cycles translating the data back and forth. More importantly, many JSON server frameworks read the entire body into memory before parsing it. A handful of concurrent large uploads can swamp a modest server because each one is being held in RAM as an overweight text string before it even gets saved to disk. Base64 works in a pinch, but it was never designed to carry heavy files at production scale.

The better answer is multipart/form-data. This format treats a single HTTP request as a collection of separate parts, each divided by a unique boundary string. One part might hold a plain text field. The next part might hold a raw binary image, marked with its own Content-Type and Content-Disposition headers. The server reads the incoming stream sequentially, watching for the boundary markers, and hands each section off to the appropriate handler without ever needing to treat the entire payload as a single block of text.

In Node.js, this distinction is especially important. The express.json() middleware knows how to parse JSON bodies, but it does not handle file streams. To process multipart uploads, you need a streaming parser like Multer or Busboy. These tools attach to the raw request stream and read it chunk by chunk. Multer, for instance, lets you choose whether to write incoming files to a temporary folder on disk or hold smaller ones in memory. That configuration decision matters. If you keep everything in memory and your app suddenly receives several large files at once, your process can run out of heap space and crash. Writing to disk trades I/O for stability, but it introduces its own questions about cleanup and path security.

For a small application, saving files to a local folder like ./uploads feels natural and fast. The file lands on the same machine running your code, and serving it back is just a matter of pointing at the right path. This works right up until it does not.

The moment you place a load balancer in front of a second application server, local storage becomes a bug. A user uploads a profile picture. The load balancer routes the request to Server A, and the file is written to Server A's disk. Later, that user asks to view the image, but the load balancer sends the request to Server B. Server B checks its own filesystem and finds nothing. The file is effectively missing. You can implement sticky sessions to pin a user to the same machine, but that is a fragile fix. If Server A restarts, gets redeployed, or is replaced by an autoscaling instance, the data vanishes. In containerized environments, local disks are even more ephemeral. A Docker container’s filesystem is meant to be disposable. Treating it as permanent storage is a reliable way to lose user data.

The standard solution is to separate compute from storage. You keep your application servers stateless and send uploaded files to dedicated object storage like AWS S3 or Google Cloud Storage. These services are built for durability, geographic distribution, and massive concurrency. The application server handles the request, validates the metadata, and then hands the bytes off to infrastructure designed specifically to hold them.

ومع ذلك، فإن هذا النمط نفسه قد يخلق عنق زجاجة إذا تم تنفيذه بإهمال. تبدأ العديد من الفرق بجعل المتصفح يرفع الملف إلى الخلفية (backend)، ثم تقوم الخلفية بتمرير كل بايت إلى وحدة التخزين الكائنية (object storage). إذا قام مستخدم برفع فيديو بحجم خمسمائة ميجابايت، فسيصبح خادمك وسيطاً؛ فهو يستهلك عرض النطاق الترددي (bandwidth) أثناء سحب الملف، ثم يستهلك المزيد من عرض النطاق الترددي أثناء دفعه إلى S3. ويظل الاتصال مفتوحاً طوال مدة عملية النقل. كما أن عمليات الرفع البطيئة من المستخدمين الذين يعانون من ظروف شبكة سيئة يمكن أن تشغل اتصالات الخادم لعدة دقائق. ويظل استهلاك الذاكرة مرتفعاً إذا قام الخادم بتخزين التدفق مؤقتاً (buffering the stream)، وإذا كنت تعمل على استضافة بنظام الدفع حسب الاستخدام (metered hosting)، فستدفع مرتين مقابل نفس عملية نقل البيانات. ولا يحل التوسع الأفقي (Horizontal scaling) هذه المشكلة، لأن كل خادم إضافي تضيفه سيظل عالقاً في نقل البايتات التي لا يحتاج إلى رؤيتها.

تحل الأنظمة الحديثة هذه المشكلة عن طريق إزالة الخلفية (backend) تماماً من مسار البيانات. فبدلاً من قبول الملف، تكتفي الخلفية بقبول طلب للحصول على إذن بالرفع. يبدو سير العمل كالتالي:

  • يطلب المتصفح من الخلفية بدء عملية رفع، وعادةً ما يرسل فقط اسم الملف، ونوعه، والغرض المقصود منه.
  • تقوم الخلفية بمصادقة المستخدم، والتحقق من صحة الطلب وفقاً لقواعد العمل، وتستخدم SDK لإنشاء URL مؤقت موقع مسبقاً (presigned URL) من مزود وحدة التخزين الكائنية.
  • تعيد الخلفية ذلك الـ URL إلى المتصفح. يكون الـ URL محصوراً في bucket و key محددين، وصالحاً لفترة قصيرة مثل خمس أو خمس عشرة دقيقة، وموقعاً برمز (token) يمنح فقط الأذونات الدقيقة المطلوبة.
  • يقوم المتصفح برفع الملف مباشرة إلى S3 أو GCS باستخدام PUT أو POST قياسي. تنتقل البايتات مباشرة من جهاز المستخدم إلى