Web awal berjalan menggunakan teks biasa. Anda mengetikkan nama pengguna atau komentar ke dalam formulir, menekan kirim, dan sebuah string pendek dikirim ke server melalui HTTP. Siklus request-response sederhana tersebut mendefinisikan infrastrukturnya. Ketika orang-orang mulai ingin berbagi foto, dokumen, dan video, para insinyur harus mencari cara untuk memindahkan data biner mentah melalui sistem yang sepenuhnya dibangun untuk teks yang dapat dibaca.
Jika Anda mencoba memasukkan JPEG ke dalam objek JSON, Anda akan membentur dinding fundamental. JSON adalah protokol teks. Ia mengharapkan karakter Unicode yang valid, tanda kutip, kurung kurawal, dan string yang di-escape dengan benar. File biner hanyalah urutan byte yang panjang, yang banyak di antaranya tidak memiliki representasi yang dapat dicetak. Memasukkan byte-byte tersebut secara paksa ke dalam string JSON akan merusak parser, urutan escape akan merusak payload, dan seluruh pesan menjadi tidak terbaca di sisi penerima.
Encoding Base64 muncul sebagai solusi sementara yang jelas. Ia memetakan ulang data biner ke dalam set terbatas dari enam puluh empat karakter ASCII yang dapat dicetak. Setiap tiga byte biner menjadi empat karakter teks. Payload tersebut kini menjadi JSON yang valid, yang berarti ia akan berhasil melewati API standar apa pun. Namun, biayanya langsung terasa. Re-encoding tersebut membengkakkan ukuran file sekitar tiga puluh tiga persen. Gambar berukuran tiga megabyte menjadi empat megabyte saat dikirimkan. Baik klien maupun server menghabiskan siklus CPU ekstra untuk menerjemahkan data bolak-balik. Yang lebih penting, banyak framework server JSON membaca seluruh body ke dalam memori sebelum melakukan parsing. Beberapa unggahan besar secara bersamaan dapat melumpuhkan server yang sederhana karena masing-masing ditahan di RAM sebagai string teks yang terlalu berat sebelum sempat disimpan ke disk. Base64 berguna dalam keadaan mendesak, tetapi tidak pernah dirancang untuk membawa file berat dalam skala produksi.
Jawaban yang lebih baik adalah multipart/form-data. Format ini memperlakukan satu permintaan HTTP sebagai kumpulan bagian terpisah, yang masing-masing dipisahkan oleh string boundary yang unik. Satu bagian mungkin berisi field teks biasa. Bagian berikutnya mungkin berisi gambar biner mentah, yang ditandai dengan header Content-Type dan Content-Disposition miliknya sendiri. Server membaca aliran (stream) yang masuk secara berurutan, memantau penanda boundary, dan menyerahkan setiap bagian ke handler yang sesuai tanpa perlu memperlakukan seluruh payload sebagai satu blok teks tunggal.
Di Node.js, perbedaan ini sangatlah penting. Middleware express.json() tahu cara melakukan parsing body JSON, tetapi tidak menangani file stream. Untuk memproses unggahan multipart, Anda memerlukan streaming parser seperti Multer atau Busboy. Alat-alat ini menempel pada raw request stream dan membacanya potongan demi potongan (chunk by chunk). Multer, misalnya, memungkinkan Anda memilih apakah akan menulis file yang masuk ke folder sementara di disk atau menahan file yang lebih kecil di memori. Keputusan konfigurasi tersebut sangat berpengaruh. Jika Anda menyimpan semuanya di memori dan aplikasi Anda tiba-tiba menerima beberapa file besar sekaligus, proses Anda dapat kehabisan heap space dan crash. Menulis ke disk menukar I/O dengan stabilitas, tetapi hal ini menimbulkan pertanyaan tersendiri tentang pembersihan (cleanup) dan keamanan path.
Untuk aplikasi kecil, menyimpan file ke folder lokal seperti ./uploads terasa alami dan cepat. File tersebut mendarat di mesin yang sama yang menjalankan kode Anda, dan menyajikannya kembali hanyalah masalah mengarahkan ke path yang benar. Ini berfungsi sampai akhirnya tidak lagi berfungsi.
Saat Anda menempatkan load balancer di depan server aplikasi kedua, penyimpanan lokal menjadi sebuah bug. Seorang pengguna mengunggah foto profil. Load balancer mengarahkan permintaan ke Server A, dan file tersebut ditulis ke disk Server A. Kemudian, pengguna tersebut meminta untuk melihat gambar, tetapi load balancer mengirimkan permintaan ke Server B. Server B memeriksa filesystem-nya sendiri dan tidak menemukan apa pun. File tersebut secara efektif hilang. Anda dapat menerapkan sticky sessions untuk mengunci pengguna ke mesin yang sama, tetapi itu adalah perbaikan yang rapuh. Jika Server A dimulai ulang, dideploy ulang, atau diganti oleh instance autoscaling, datanya akan lenyap. Dalam lingkungan tercontainerisasi, disk lokal bahkan lebih bersifat efemeral. Filesystem kontainer Docker dimaksudkan untuk dapat dibuang (disposable). Menganggapnya sebagai penyimpanan permanen adalah cara yang pasti untuk kehilangan data pengguna.
Solusi standarnya adalah memisahkan komputasi dari penyimpanan. Anda menjaga server aplikasi Anda tetap stateless dan mengirimkan file yang diunggah ke object storage khusus seperti AWS S3 atau Google Cloud Storage. Layanan-layanan ini dibangun untuk durabilitas, distribusi geografis, dan konkurensi massal. Server aplikasi menangani permintaan, memvalidasi metadata, lalu menyerahkan byte tersebut ke infrastruktur yang dirancang khusus untuk menyimpannya.
Namun, pola ini pun dapat menciptakan bottleneck jika Anda mengimplementasikannya dengan sembrono. Banyak tim memulai dengan membiarkan browser mengunggah file ke backend, lalu backend meneruskan setiap byte ke object storage. Jika seorang pengguna mengunggah video berukuran lima ratus megabyte, server Anda akan menjadi perantara. Server mengonsumsi bandwidth saat menarik file masuk, lalu mengonsumsi lebih banyak bandwidth saat mendorongnya keluar ke S3. Koneksi tetap terbuka selama seluruh durasi transfer. Unggahan yang lambat dari pengguna dengan kondisi jaringan yang buruk dapat menyita koneksi server selama beberapa menit. Penggunaan memori tetap tinggi jika server melakukan buffering pada stream, dan jika Anda menggunakan metered hosting, Anda membayar dua kali untuk transfer data yang sama. Scaling horizontal tidak menyelesaikan masalah ini, karena setiap server tambahan yang Anda tambahkan tetap akan terjebak mengangkut byte yang sebenarnya tidak perlu dilihat oleh server tersebut.
Sistem modern menyelesaikan masalah ini dengan mengeluarkan backend sepenuhnya dari jalur data. Alih-alih menerima file, backend hanya menerima permintaan izin untuk mengunggah. Alurnya adalah sebagai berikut:
- Browser meminta backend untuk memulai unggahan, biasanya hanya mengirimkan nama file, tipe file, dan tujuan penggunaan.
- Backend mengautentikasi pengguna, memvalidasi permintaan terhadap aturan bisnis, dan menggunakan SDK untuk menghasilkan presigned URL sementara dari penyedia object storage.
- Backend mengembalikan URL tersebut ke browser. URL tersebut dibatasi pada bucket dan key tertentu, berlaku dalam jangka waktu singkat seperti lima atau lima belas menit, dan ditandatangani dengan token yang hanya memberikan izin presisi yang diperlukan.
- Browser mengunggah file secara langsung ke S3 atau GCS menggunakan PUT atau POST standar. Byte tersebut langsung berpindah dari perangkat pengguna ke
