Yazılım01.06.20267 dk okuma4

React ve Express ile Dosya Yükleme Mimarisi: Protokoller, İletim Türleri ve Performans Yönetimi

#React#Express#Multer#File Upload#Multipart Form Data

Giriş: Dosya İletiminde Protokol ve Mimari Seçimleri

Web uygulamalarında istemci (Frontend) ve sunucu (Backend) arasındaki veri transferi, taşınacak verinin türüne göre optimize edilmek zorundadır. Kullanıcı adı, şifre veya form girdileri gibi metin tabanlı veriler standart transfer protokolleriyle kolayca iletilirken; görsel, PDF veya video gibi ham ikili (binary) dosyaların aktarımı ağ seviyesinde farklı yaklaşımlar gerektirir.

Bu yazı dizisinde, React ve Express.js teknolojilerini kullanarak dosya yükleme mimarilerini, transfer protokollerini ve tarayıcı seviyesindeki performans optimizasyonlarını teknik detaylarıyla inceleyeceğiz.


1. Metin Tabanlı İletim: Base64 ve FileReader Yaklaşımı

Dosya transferinde ilk yöntem, ikili (binary) veriyi istemci tarafında metne dönüştürerek standart veri akışlarına dahil etmektir. Bu işlem için tarayıcıda yerleşik olarak bulunan FileReader API'si kullanılır.

FileReader, hedef dosyayı okuyarak data:image/jpeg;base64,/9j/4AAQ... biçiminde bir metin dizesine (Base64 string) dönüştürür.


Mimari İşleyiş ve Sunucu Tarafı Etkisi

  • Doğrudan Alım: Express.js sunucusu, gelen bu veriyi standart bir metin parametresi olarak kabul eder.

  • Middleware Bağımsızlığı: Sunucu tarafında multipart/form-data ayrıştırıcılarına (Multer vb.) ihtiyaç duyulmaz. Veri, express.json() middleware'i katmanında çözümlenerek doğrudan req.body nesnesine aktarılır.

  • Disk I/O İşlemi: Alınan Base64 dizesi sunucu diskinde fiziksel bir dosya (.jpg, .png) olarak saklanmak istenirse, backend tarafında bu metni tekrar ikili veriye deşifre edecek (decode) bir işlem blokunun kurgulanması gerekir.

Performans Sınırı: Verinin Base64 formatına dönüştürülmesi, dosya boyutunu ağ üzerinde yaklaşık %33 oranında artırır. Büyük ölçekli dosyalarda bu durum bant genişliği (bandwidth) maliyetini artırır ve sunucunun gelen yüksek boyutlu metni parse etmesi esnasında CPU/RAM optimizasyonunu zorlaştırarak 413 Payload Too Large hatalarına sebebiyet verebilir.

2. Neden Standart JSON (application/json) Yetmez?

Web mimarisinde veri iletim standardı olan JSON (application/json), tamamen metin tabanlı (String, Number, Boolean, Object, Array, Null) yapıları işlemek üzere tasarlanmıştır.

Fiziksel bir dosya ise byte dizilerinden oluşan ham bir ikili (binary) veridir. Tarayıcı katmanı, ham bir dosya nesnesini yapısal özellikleri bozulmadan düz bir JSON dizesi içerisine gömemez. Dosyayı orijinal ikili (binary) formatıyla, boyutsal büyümeye maruz bırakmadan transfer etmek için HTTP protokolünün çok parçalı gövde yapısını (multipart/form-data) kullanmak mimari bir gerekliliktir.

3. JavaScript Yerleşik FormData Nesnesi

FormData, tarayıcı ortamında (klasik veya modern frontend framework'lerinde) tamamen yerleşik olarak bulunan standart bir JavaScript kurucu nesnesidir (Constructor). Üçüncü parti bir kütüphane bağımlılığı olmaksızın new FormData() arabirimiyle çağrılır.

FormData nesnesinin temel görevi, HTML form elemanlarının davranışını kod seviyesinde simüle ederek hem düz metinleri hem de ham dosya nesnelerini (Blob / File) orijinal ikili yapılarını koruyarak paketlemektir. İstemci tarafında veriler FormData nesnesine eklendiğinde, tarayıcı HTTP istek başlığındaki (Request Header) Content-Type alanını otomatik olarak multipart/form-data şeklinde yapılandırır.

4. Protokol Karşılaştırması: application/json vs multipart/form-data

Teknik Kriter

application/json

multipart/form-data

Paketleme Biçimi

Tek parça, süslü parantezlerden oluşan ardışık metin dizesi.

Sınır çizgileriyle (boundary) ayrılmış çok parçalı gövde yapısı.

Veri Uyumluluğu

Sadece metin tabanlı veriler (String, Number vb.).

Hem metin tabanlı veriler hem de ham ikili (binary) dosyalar.

Ağ Gövde Yapısı

Tüm parametreler tek bir JSON objesi içindedir.

Her bir veri segmenti (Parça 1: id_dec, Parça 2: name, Parça 3: image ikili akışı) bağımsız alanlar olarak taşınır.

Express Çözümleme

app.use(express.json()) yerleşik fonksiyonu yeterlidir.

Express çekirdeği yetersizdir; Multer middleware entegrasyonu zorunludur.

Bölüm 1: Frontend (React + TypeScript) İstek Katmanının İnşası

Teorik arka planı kurduktan sonra, ilk olarak istemci tarafında ham veriyi paketleyip ağa fırlatacak React bileşenimizi yapılandıralım. Burada TypeScript tip güvenliğini (interface) elden bırakmıyoruz.

1.1 FormData Akışının Kodlanması

Frontend mimarisinde ham dosyayı sunucuya göndermek için yerleşik FormData nesnesini kullanırız. Burada en kritik kural, frontend'de append edilen anahtar isminin (bizim senaryomuzda "image"), backend tarafında Multer'ın beklediği isimle birebir eşleşmesi zorunluluğudur.

// frontend/src/App.tsx
import React, { useState } from "react";
import axios from "axios";

// Form alanları için tip tanımı
interface PersonnelFormValues {
  name: string;
}

export default function App() {
  const [name, setName] = useState<string>("");
  const [selectedFile, setSelectedFile] = useState<File | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    // JavaScript yerleşik FormData nesnesi başlatılıyor
    const formData = new FormData();
    formData.append("name", name); // Standart string verisi ekleme
    
    if (selectedFile) {
      // DİKKAT: 'image' anahtarı backend katmanındaki Multer ismiyle eşleşmelidir!
      formData.append("image", selectedFile); // Ham binary dosya nesnesi ekleme
    }

    try {
      // Request gönderimi (Tarayıcı Content-Type'ı otomatik multipart/form-data yapar)
      const response = await axios.post("http://localhost:5000/api/personnel", formData);
      alert(response.data.message);
    } catch (error) {
      console.error("İstek iletim hatası:", error);
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "10px", maxWidth: "300px" }}>
      <input type="text" placeholder="Personel İsmi" onChange={(e) => setName(e.target.value)} />
      <input type="file" accept="image/*" onChange={(e) => setSelectedFile(e.target.files?.[0] || null)} />
      <button type="submit">Veriyi Gönder</button>
    </form>
  );
}

Bölüm 2: Backend (Express.js + Multer) Çözümleme Katmanı

Express.js ekosisteminde, kapıya gelen çok parçalı HTTP istek paketlerini parçalayıp yönetmek için Multer ara yazılımını devreye alıyoruz.

2.1 Temel Sunucu Yapılandırması

// backend/src/app.ts
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import multer from 'multer';
import path from 'path';

const app = express();
app.use(cors());
app.use(express.json()); // Standart application/json istekleri için

// 1. Multer Disk Depolama Motoru Yapılandırması
const fileStorage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'images'); // Fiziksel kayıt klasörü
    },
    filename: (req, file, cb) => {
        // Çakışmayı önlemek için milisaniye damgası ve rastgele hash kombinasyonu
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
        cb(null, uniqueSuffix + path.extname(file.originalname)); 
    }
});

const upload = multer({ storage: fileStorage });

// 2. Rota Katmanında Middleware Kullanımı ve Yakalama
app.post('/api/personnel', upload.single('image'), (req: Request, res: Response, next: NextFunction): any => {
    try {
        const { name } = req.body; // Multer düz metinleri parse edip req.body'ye yazdı
        const file = req.file;     // Multer ham dosyayı işleyip req.file nesnesine yazdı

        if (!file) {
            return res.status(400).json({ message: "Lütfen geçerli bir dosya yükleyin." });
        }

        console.log(`Fiziksel Yol (file.path): ${file.path}`);
        console.log(`MIME Türü (file.mimetype): ${file.mimetype}`);

        return res.status(201).json({ 
            message: `Veriler ve ${file.filename} dosyası sunucu diskine başarıyla yazıldı!` 
        });
    } catch (error) {
        next(error);
    }
});

// Global Hata İşleyici
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
    res.status(500).json({ message: err.message || "Teknik aksaklık oluştu." });
});

app.listen(5000, () => console.log(' Sunucu 5000 portunda aktif.'));

2.2 Derinlemesine Multer Mimarisi ve Kritik Notlar

Yazdığımız bu backend kodunun arka planında dönen süreçleri ve kararları 3 ana başlıkta inceleyelim:

A) Bellek (Buffer) Yönetimi ve RAM Riskleri

Eğer Multer'ı başlatırken hiçbir opsiyon geçmezsek (örn: const upload = multer()), gelen veri akışı (Stream) sunucunun RAM belleğinde Buffer (Arabellek) nesnesi olarak toplanır.

  • Mimari Tehlike: Yüksek trafikli anlarda veya çok büyük boyutlu dosyalarda (örn: 100MB video yüklemeleri), dosyaların bütünüyle RAM'e yüklenmesi sunucunun bellek eşiğini (Memory Footprint) bir anda patlatır. Bu durum Node.js sürecinin Out of Memory hatası vererek tamamen çökmesine (crash) yol açar.

B) dest Parametresinin Kolaylığı ve Üretim (Production) Sınırları

Kodu hızlıca ayağa kaldırmak için multer({ dest: 'uploads/' }) kısa yolu tercih edilebilir. Bu durumda veri akışı RAM'de şişmek yerine doğrudan disk I/O operasyonuyla klasöre yazılır ve RAM korunur.

  • Kısıtlama: Ancak Multer, dosya isimlerinin çakışmasını (Naming Collision) ve işletim sistemi düzeyindeki güvenlik açıklarını engellemek adına dosyaları uzantısız (extensionless) rastgele bir hash dizesi olarak kaydeder. Dosya uzantısı (.png, .jpg) kaybolduğu için, üretim ortamında bu dosyaları istemciye tekrar servis ederken MIME-type doğrulaması yapmak ve dosyayı tarayıcıya düzgün okutmak imkansız hale gelir. Bu yüzden profesyonel projelerde daima diskStorage kullanılır.

C) Neden return Yerine cb (Callback) Mekanizması?

diskStorage içindeki fonksiyonların direkt string döndürmek (return "uploads") yerine bir cb fonksiyonu tetiklemesi, Node.js'in asenkron (eşzamansız) doğasıyla ilgilidir. Dosya diske yazılmadan hemen önce asenkron bir süreç işletmek isteyebiliriz:

  • Gelen isteğe göre veritabanına (async/await) sorgu atıp ilgili personele ait özel bir klasör adı çekmek.

  • Dosya sistemini (fs) kontrol edip dinamik klasör oluşturmak.

Düz bir return ifadesi asenkron işlemlerin bitmesini bekleyemezken, Error-First Callback standardındaki cb fonksiyonu sayesinde Multer bizim asenkron operasyonlarımızın tamamlanmasını sabırla bekler. Biz ne zaman cb(null, sonuc) dersek, Multer I/O operasyonuna o milisaniyede devam eder.

2.3 Veri Tabanı Saklama Stratejisi (Mühendislik Kuralı)

Görsel, PDF veya video gibi büyük boyutlu ikili veriler (binary) doğrudan veritabanında (BLOB formatında) saklanmamalıdır.

  • Nedeni: Dosyaları DB hücrelerinde tutmak sorgu ve indeksleme performansını düşürür, veritabanı yedeği almayı (backup) devasa boyutlardan ötürü imkansızlaştırır ve sunucu maliyetlerini artırır.

  • Doğru Yaklaşım: Dosya, sunucunun fiziksel dosya sistemine (ya da AWS S3 gibi bir bulut depolama servisine) yazılır; veritabanına ise sadece o dosyaya erişimi sağlayacak olan string tipindeki göreli dosya yolu (file.path) kaydedilir.