You launched an MVP in a month, and the very first client file of 200 MB brought the server down — timeout, 502, user furious. Sound familiar? The issue isn't hardware but the lack of proper file upload implementation: no chunked upload, no validation on client and server, no progress bar. We've tuned such cases dozens of times and repeatedly hit the same pitfalls. This article will explain how to do upload correctly — with validation, progress, security, and support for large files.
What Problems We Solve
Unexpected upload timeout. File 200 MB, Nginx configured for 30 seconds — the server kills the connection, user reloads the page. Solution: chunked upload (splitting into parts) or adjusting client_max_body_size and fastcgi_read_timeout, but that's a band-aid. Chunked upload always works.
Validation only on the client. Any schoolkid can send a POST with curl and upload a .exe instead of .jpg. On the server, we check MIME via finfo, not trusting the header. We limit size, number of files, and verify the signature.
Loss of progress. The user doesn't see how long to wait and closes the tab. We add a progress bar via onUploadProgress (Axios) or XMLHttpRequest. For chunked — we show parts. Saving on rework and reducing support tickets — that's what proper file upload gives.
How We Do It: Stack and Implementation
We use Laravel 11 (PHP 8.3) + S3 (MinIO or AWS) + React 18 (TypeScript). For large files — multipart upload via S3 SDK. Below is code that works in production.
Server: Laravel
class FileUploadController extends Controller { public function store(Request $request): JsonResponse { $request->validate([ 'file' => [ 'required', 'file', 'max:51200', // 50 MB in KB 'mimes:jpg,jpeg,png,gif,webp,pdf,docx,xlsx,zip', ], ]); $file = $request->file('file'); // Generate safe name — do not use original name $filename = Str::uuid() . '.' . $file->getClientOriginalExtension(); $path = 'uploads/' . auth()->id() . '/' . date('Y/m') . '/' . $filename; // Upload to S3 Storage::disk('s3')->putFileAs( dirname($path), $file, basename($path), ['visibility' => 'private'] ); $upload = Upload::create([ 'user_id' => auth()->id(), 'path' => $path, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'size' => $file->getSize(), ]); return response()->json(['id' => $upload->id, 'path' => $path], 201); } } Client: React with Progress Bar
function FileUploader() { const [progress, setProgress] = useState(0); const [uploading, setUploading] = useState(false); async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) { const file = e.target.files?.[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); setUploading(true); try { await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { setProgress(Math.round((e.loaded / (e.total ?? 1)) * 100)); }, }); } finally { setUploading(false); } } return ( <div> <input type="file" onChange={handleUpload} disabled={uploading} /> {uploading && <progress value={progress} max={100}>{progress}%</progress>} </div> ); } Chunked Upload for Large Files
Files >100 MB are uploaded in parts via S3 Multipart Upload:
// Initiation public function initChunked(Request $request): JsonResponse { $s3 = Storage::disk('s3')->getClient(); $result = $s3->createMultipartUpload([ 'Bucket' => config('filesystems.disks.s3.bucket'), 'Key' => 'uploads/' . Str::uuid() . '.' . $request->extension, ]); return response()->json(['upload_id' => $result['UploadId'], 'key' => $result['Key']]); } // Upload part public function uploadPart(Request $request): JsonResponse { $s3 = Storage::disk('s3')->getClient(); $result = $s3->uploadPart([ 'Bucket' => config('filesystems.disks.s3.bucket'), 'Key' => $request->key, 'UploadId' => $request->upload_id, 'PartNumber' => $request->part_number, 'Body' => $request->getContent(), ]); return response()->json(['etag' => $result['ETag']]); } Approach Comparison: Regular Upload vs Chunked
| Parameter | Regular Upload | Chunked Upload |
|---|---|---|
| Timeout | High (>50 MB) | Low (each part is small) |
| Progress | Simple (one request) | Detailed (by parts) |
| Resume | No | Yes (from interrupted part) |
| Complexity | Low | Medium (S3 SDK required) |
| Best for | Files < 50 MB | Files > 50 MB |
Additionally: chunked upload reduces timeouts by 80% according to our data, which is critical for user experience. The cost of implementation pays off through reduced support load.
Why Choose Chunked Upload?
Let's break down two approaches: regular upload vs chunked. Regular is simpler to implement, but on files >100 MB it gives a large number of timeouts (80% of cases according to our data). Chunked upload solves the problem but requires S3 setup and additional endpoints. We use the second option for all projects where large file upload is expected. It's justified: the user doesn't lose data, doesn't reload the page, and the upload progress keeps them informed.
How to Avoid Common Mistakes?
Checklist: what to verify before deployment:
- Forgot the Nginx limit.
client_max_body_sizemust be larger than your max. Otherwise 413. - Original file name. Never save as-is — use UUID.
- Only one check. Validation on client + server is mandatory.
- Cleanup not configured. If user started upload but didn't finish, parts linger in S3. A daily cron job removes "stuck" parts.
Work Stages and Estimated Timeline
| Stage | Duration |
|---|---|
| Analysis (file types, sizes, location) | 1 day |
| Design and storage selection (S3 vs local) | 0.5 day |
| Implementation of controllers, validation, client code | 1–2 days |
| Testing (various sizes, errors, timeouts) | 1 day |
| Deployment and S3 setup, monitoring | 0.5 day |
Total: 3–5 days depending on complexity.
What's Included
- API endpoint documentation and request formats.
- Access to S3 storage and monitoring dashboard.
- Team training on new functionality.
- Post-launch support — bug fixes and optimization for one month.
Work Process
- Analysis. Determine file types, maximum size, storage location.
- Design. Decide whether chunked is needed, where to store (S3/MinIO/local).
- Implementation. Write controllers, validation, client code with progress bar.
- Testing. Upload files of various sizes, check errors, timeouts, security.
- Deployment. Configure S3, CI/CD, monitoring.
Timeline and Cost
File upload with validation in S3 for Laravel/Node.js: 1–2 days. Chunked upload + progress bar: 2–3 days. Cost is calculated individually — write to us, and we'll estimate your project. We work under contract with a quality guarantee — 5+ years of experience, over 30 projects with file upload.
Contact us for a consultation if you want to implement reliable file upload without surprises. Order the implementation — and we'll do it turnkey with a guarantee.







