Web awal beroperasi menggunakan teks biasa. Anda menaip nama pengguna atau komen ke dalam borang, tekan hantar, dan satu rentetan pendek dihantar ke pelayan melalui HTTP. Kitaran permintaan-respons yang ringkas itu menentukan infrastrukturnya. Apabila orang mula mahu berkongsi foto, dokumen, dan video, jurutera perlu mencari jalan bagaimana untuk memindahkan data binari mentah melalui sistem yang dibina sepenuhnya untuk teks yang boleh dibaca.
Jika anda cuba memasukkan JPEG ke dalam objek JSON, anda akan menghadapi halangan asas. JSON ialah protokol teks. Ia memerlukan aksara Unicode yang sah, pembuka/penutup kata, kurungan, dan rentetan yang di-escape dengan betul. Fail binari hanyalah satu urutan bait yang panjang, yang kebanyakannya tidak mempunyai representasi yang boleh dicetak. Masukkan bait-bait tersebut ke dalam rentetan JSON dan parser akan rosak, urutan escape akan merosakkan payload, dan keseluruhan mesej menjadi tidak boleh dibaca di pihak sebelah sana.
Pengekodan Base64 muncul sebagai jalan penyelesaian yang jelas. Ia memetakan semula data binari kepada set terhad enam puluh empat aksara ASCII yang boleh dicetak. Setiap tiga bait binari menjadi empat aksara teks. Payload kini merupakan JSON yang sah, bermakna ia akan berjaya melalui mana-mana API standard. Namun, kosnya adalah serta-merta. Pengekodan semula itu mengembangkan saiz fail sebanyak kira-kira tiga puluh tiga peratus. Imej tiga megabait menjadi empat megabait semasa penghantaran. Kedua-dua klien dan pelayan menggunakan kitaran CPU tambahan untuk menterjemah data tersebut berulang-alik. Lebih penting lagi, banyak rangka kerja (framework) pelayan JSON membaca keseluruhan kandungan (body) ke dalam memori sebelum melakukan parsing. Beberapa muat naik besar secara serentak boleh melumpuhkan pelayan yang sederhana kerana setiap satu disimpan dalam RAM sebagai rentetan teks yang berat sebelum ia sempat disimpan ke dalam cakera. Base64 berfungsi dalam keadaan terdesak, tetapi ia tidak pernah direka untuk membawa fail berat pada skala pengeluaran (production scale).
Jawapan yang lebih baik ialah multipart/form-data. Format ini melayan satu permintaan HTTP sebagai koleksi bahagian yang berasingan, di mana setiap bahagian dibahagikan oleh rentetan sempadan (boundary string) yang unik. Satu bahagian mungkin mengandungi medan teks biasa. Bahagian seterusnya mungkin mengandungi imej binari mentah, yang ditandakan dengan pengepala Content-Type dan Content-Disposition tersendiri. Pelayan membaca aliran (stream) yang masuk secara berturutan, memerhatikan penanda sempadan, dan menyerahkan setiap bahagian kepada pengendali (handler) yang sesuai tanpa perlu melayan keseluruhan payload sebagai satu blok teks tunggal.
Dalam Node.js, perbezaan ini amat penting. Middleware express.json() tahu cara untuk melakukan parsing pada kandungan JSON, tetapi ia tidak mengendalikan aliran fail (file streams). Untuk memproses muat naik multipart, anda memerlukan streaming parser seperti Multer atau Busboy. Alatan ini menyambung ke aliran permintaan mentah dan membacanya bahagian demi bahagian (chunk by chunk). Multer, sebagai contoh, membolehkan anda memilih sama ada untuk menulis fail yang masuk ke dalam folder sementara pada cakera atau menyimpan fail yang lebih kecil di dalam memori. Keputusan konfigurasi tersebut adalah penting. Jika anda menyimpan segalanya dalam memori dan aplikasi anda tiba-tiba menerima beberapa fail besar sekaligus, proses anda boleh kehabisan ruang heap dan terhenti (crash). Menulis ke cakera menukar I/O demi kestabilan, tetapi ia menimbulkan persoalan tersendiri tentang pembersihan dan keselamatan laluan (path security).
Untuk aplikasi kecil, menyimpan fail ke dalam folder tempatan seperti ./uploads terasa semula jadi dan pantas. Fail tersebut disimpan pada mesin yang sama yang menjalankan kod anda, dan menyajikannya semula hanyalah soal merujuk ke laluan yang betul. Ini berfungsi sehinggalah ia tidak lagi berfungsi.
Sebaik sahaja anda meletakkan pengimbang beban (load balancer) di hadapan pelayan aplikasi kedua, storan tempatan akan menjadi satu pepijat (bug). Seorang pengguna memuat naik gambar profil. Pengimbang beban menghalakan permintaan ke Pelayan A, dan fail tersebut ditulis ke cakera Pelayan A. Kemudian, pengguna tersebut meminta untuk melihat imej itu, tetapi pengimbang beban menghantar permintaan ke Pelayan B. Pelayan B menyemak sistem failnya sendiri dan tidak menemui apa-apa. Fail tersebut secara berkesan telah hilang. Anda boleh melaksanakan sticky sessions untuk menetapkan pengguna pada mesin yang sama, tetapi itu adalah penyelesaian yang rapuh. Jika Pelayan A dimulakan semula, dikerahkan semula (redeployed), atau digantikan oleh instans penskalaan automatik (autoscaling instance), data tersebut akan hilang. Dalam persekitaran berbekas (containerized environments), cakera tempatan adalah lebih bersifat sementara (ephemeral). Sistem fail bekas Docker bertujuan untuk dibuang (disposable). Menganggapnya sebagai storan kekal adalah cara yang pasti untuk kehilangan data pengguna.
Penyelesaian standard adalah dengan memisahkan pengkomputeran daripada storan. Anda mengekalkan pelayan aplikasi anda sebagai tanpa keadaan (stateless) dan menghantar fail yang dimuat naik ke storan objek khusus seperti AWS S3 atau Google Cloud Storage. Perkhidmatan ini dibina untuk ketahanan, pengagihan geografi, dan konkurensi yang besar. Pelayan aplikasi mengendalikan permintaan, mengesahkan metadata, dan kemudian menyerahkan bait-bait tersebut kepada infrastruktur yang direka khusus untuk menyimpannya.
Yet even this pattern creates a bottleneck if you implement it carelessly. Many teams start by having the browser upload the file to the backend, and then having the backend forward every single byte to object storage. If a user uploads a five-hundred megabyte video, your server becomes a middleman. It consumes bandwidth pulling the file in, then consumes more bandwidth pushing it out to S3. The connection stays open for the entire duration of the transfer. Slow uploads from users with poor network conditions can tie up server connections for minutes. Memory usage stays elevated if the server buffers the stream, and if you are running on metered hosting, you are paying twice for the same data transfer. Horizontal scaling does not solve this, because every additional server you add still gets stuck ferrying bytes it does not need to see.
Modern systems solve the problem by removing the backend from the data path entirely. Instead of accepting the file, the backend only accepts a request for permission to upload. The flow looks like this:
- The browser asks the backend to initiate an upload, usually sending only the filename, file type, and intended purpose.
- The backend authenticates the user, validates the request against business rules, and uses an SDK to generate a temporary presigned URL from the object storage provider.
- The backend returns that URL to the browser. The URL is scoped to a specific bucket and key, valid for a short window such as five or fifteen minutes, and signed with a token that grants only the precise permissions needed.
- The browser uploads the file directly to S3 or GCS using a standard PUT or POST. The bytes travel straight from the user’s device to
