Strategi REST API File Upload (Laravel + Flutter)
Artikel ini membahas strategi REST API File Upload menggunakan Laravel (local & S3) serta implementasi di sisi Flutter dengan Dio. Setiap strategi akan dijelaskan kelebihan & kekurangan sebelum diberikan contoh implementasi kode.
π 1. Strategi File Uploadβ
1οΈβ£ File sebagai Properti dalam Resource Induk (Base64 di JSON)β
Contoh: File dikirim dalam bentuk base64 string di dalam payload JSON.
Kelebihan
- Payload tetap JSON β lebih sederhana integrasi FE & BE.
- Tidak perlu endpoint khusus untuk file.
- Mudah di-testing pakai Postman.
Kekurangan
- Payload membengkak Β±33% karena encoding base64.
- Tidak efisien untuk file besar (upload lambat, memory tinggi).
- Membebani API Gateway & BE (decode base64 β simpan binary).
- β Tidak mendukung progress upload secara akurat (karena file sudah berupa string besar).
Contoh Payload
{
"name": "John Doe",
"profile_pic": "data:image/jpeg;base64,/9j/4AAQSkZ..."
}
Laravel Controller (decode base64)
public function store(Request $request)
{
$request->validate(['profile_pic' => 'required|string']);
$imageData = base64_decode(preg_replace('#^data:image/\\w+;base64,#i', '', $request->profile_pic));
$fileName = uniqid().'.jpg';
$path = storage_path('app/public/uploads/'.$fileName);
file_put_contents($path, $imageData);
return [
'file_name' => $fileName,
'file_url' => asset('storage/uploads/'.$fileName),
];
}
Kekurangan di kode: konsumsi memory besar kalau file >5MB.
2οΈβ£ File sebagai Child Resource (Multipart di Endpoint Tertentu)β
Contoh: File diupload langsung via multipart ke endpoint resource induk.
Kelebihan
- Lebih efisien daripada base64 (langsung binary).
- Endpoint lebih semantik (misal
/users/{id}/avatar
). - β
Mendukung progress upload (Dio β
onSendProgress
).
Kekurangan
- Endpoint jadi lebih banyak kalau resource punya banyak file.
- Tidak reusable lintas resource.
Contoh Request cURL
curl -X POST "https://api.example.com/users/123/avatar" \
-H "Content-Type: multipart/form-data" \
-F "[email protected]"
Laravel Controller
public function uploadAvatar(Request $request, $id)
{
$request->validate(['file' => 'required|file|mimes:jpg,png|max:2048']);
$path = $request->file('file')->store("avatars/$id", 'public');
return [
'file_name' => basename($path),
'file_url' => asset('storage/'.$path)
];
}
Kekurangan di kode: setiap resource butuh method upload khusus.
3οΈβ£ File sebagai Resource Independen (π₯ Paling Fleksibel)β
Contoh: File punya endpoint sendiri (/uploads
), lalu direferensikan dari resource lain.
Kelebihan
- Paling fleksibel β file bisa dipakai lintas resource.
- Endpoint lebih bersih (
/uploads
,/uploads/{id}
). - Cocok untuk sistem skala besar.
- β
Mendukung progress upload (Dio β
onSendProgress
).
Kekurangan
- FE perlu dua langkah: upload file β simpan
file_id
ataufile_path
di resource induk.
Contoh Laravel Controller
public function store(Request $request)
{
$request->validate(['file' => 'required|file|mimes:jpg,png|max:2048']);
$disk = env('FILESYSTEM_DISK', 'local');
$path = $request->file('file')->store('uploads', $disk);
$url = Storage::disk($disk)->url($path);
return [
'path' => $path,
'file_name' => basename($path),
'file_url' => $url,
];
}
Catatan: tidak selalu butuh id
. Bisa juga pakai path
sebagai identifier di DB. Namun id
kadang dipakai agar lebih aman & mudah direlasikan (misalnya relasi user β avatar_file_id
).
π 2. Upload ke S3 vs Localβ
- Local β cocok untuk development atau aplikasi kecil.
- S3 β cocok untuk production, file besar, scalable.
- Laravel cukup ganti
FILESYSTEM_DISK=s3
di.env
.
Contoh S3 Upload (Laravel Controller)
$path = $request->file('file')->store('uploads', 's3');
$url = Storage::disk('s3')->url($path);
Signed Upload URL (S3 / MinIO / GCS)β
Signed URL memungkinkan FE upload langsung ke storage tanpa lewat server BE (lebih efisien & scalable).
Contoh Laravel Generate Signed URL (S3)
use Illuminate\Support\Facades\Storage;
public function getUploadUrl(Request $request)
{
$request->validate(['file_name' => 'required', 'mime' => 'required']);
$disk = Storage::disk('s3');
$path = 'uploads/' . uniqid() . '_' . $request->file_name;
$client = $disk->getDriver()->getAdapter()->getClient();
$expiry = "+10 minutes";
$command = $client->getCommand('PutObject', [
'Bucket' => env('AWS_BUCKET'),
'Key' => $path,
'ContentType' => $request->mime
]);
$presignedRequest = $client->createPresignedRequest($command, $expiry);
$uploadUrl = (string) $presignedRequest->getUri();
return [
'upload_url' => $uploadUrl,
'file_path' => $path
];
}
Flutter Upload langsung ke S3 dengan Signed URL
Future<void> uploadToSignedUrl(String filePath, String signedUrl) async {
final dio = Dio();
await dio.put(
signedUrl,
data: File(filePath).openRead(),
options: Options(
headers: {"Content-Type": "image/jpeg"},
),
onSendProgress: (sent, total) {
print("Progress: ${(sent / total * 100).toStringAsFixed(0)}%");
},
);
}
Kelebihan
- Server BE tidak menanggung beban file besar.
- Lebih scalable.
- Bisa kontrol akses private/public via policy.
- β Mendukung progress upload (karena langsung pakai Dio stream).
Kekurangan
- Lebih kompleks implementasinya (FE butuh dua step: dapat signed URL β upload file).
β Multiple File Uploadβ
- Single Endpoint bisa menerima array file (lebih simpel, tapi validasi per-file terbatas).
- Lebih baik buat endpoint tetap single-file, lalu FE lakukan upload paralel/berulang.
Contoh Multiple File Upload Laravel
public function uploadMultiple(Request $request)
{
$request->validate([
'files.*' => 'required|file|mimes:jpg,png|max:2048'
]);
$uploaded = [];
foreach ($request->file('files') as $file) {
$path = $file->store('uploads', 'public');
$uploaded[] = [
'file_name' => basename($path),
'file_url' => asset('storage/'.$path)
];
}
return $uploaded;
}
Kekurangan Multiple Upload dalam Satu Request: Validasi sulit granular (misalnya satu file gagal, semua gagal). Best practice: upload file satu-satu supaya validasi & progress lebih jelas.
π 3. Flutter (Dio) Implementasiβ
a. Upload File dengan Progressβ
final dio = Dio();
Future<void> uploadFile(String filePath) async {
String fileName = filePath.split('/').last;
FormData formData = FormData.fromMap({
"file": await MultipartFile.fromFile(filePath, filename: fileName),
});
await dio.post(
"https://api.example.com/uploads",
data: formData,
onSendProgress: (sent, total) {
print("Progress: ${(sent / total * 100).toStringAsFixed(0)}%");
},
);
}
Kelemahan: kalau strategi base64 dipakai, progress sulit dihitung akurat.
b. Upload Multiple File dari Flutterβ
Future<void> uploadMultipleFiles(List<String> paths) async {
final dio = Dio();
for (final path in paths) {
await uploadFile(path); // upload satu-satu
}
}
π 4. Ringkasan Dukungan Progress Uploadβ
Strategi | Dukungan Progress Upload |
---|---|
Base64 JSON | β Tidak akurat |
Multipart (Child) | β Didukung |
Resource Independen | β Didukung |
Signed URL (S3/MinIO/GCS) | β Didukung |
π 5. Best Practice Ringkasβ
- β Gunakan Multipart Upload untuk efisiensi.
- β Pakai resource independen untuk fleksibilitas.
- β Gunakan Presigned URL kalau file besar (langsung ke S3/minio).
- β Hindari base64 untuk file >2MB.
- β Selalu validasi file (mime, size).
- β Simpan path atau id di DB, bukan file binary.
- β Gunakan signed URL untuk kontrol akses private/public di S3. Untuk local, gunakan middleware Laravel.
- β Untuk multiple file β lebih aman upload satu-satu.
Dengan pendekatan ini, kita bisa pilih strategi sesuai kebutuhan:
- Base64 untuk prototyping cepat.
- Child Resource untuk kasus simpel.
- Resource Independen untuk sistem production skala besar.
- Signed URL untuk upload file besar & scalable.