File Upload Form Development
File upload form is one of those components where user experience and backend reliability equally matter. Bad implementation breaks on large files, doesn't provide feedback, loses data on network errors. Correct implementation works in any conditions.
What's Included
Client side:
- Drag-and-drop zone + "Select File" button
- Image preview (via
FileReaderorURL.createObjectURL) - Upload progress bar with real percentages
- Validation: file type, size, quantity
- Error handling with human-readable messages
- Upload cancellation via
AbortController
Server side:
- Multipart upload with large file support (chunked upload if needed)
- MIME type validation by content, not just extension
- Antivirus check via ClamAV or third-party API (optional)
- Storage: local, S3-compatible (MinIO, AWS S3, Cloudflare R2)
- Unique filename generation, per-user isolation
Technical Stack
| Layer | Options |
|---|---|
| UI component | React + react-dropzone, Vue + custom hook |
| HTTP upload | XMLHttpRequest (progress), fetch + ReadableStream |
| Backend | Laravel (Storage facade), Node.js (multer, busboy) |
| Storage | AWS S3, MinIO, local disk |
| Image preview | Canvas API, sharp on server |
Basic Upload with Progress
function uploadFile(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
onProgress(Math.round((e.loaded / e.total) * 100));
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Network error')));
xhr.open('POST', '/api/upload');
xhr.setRequestHeader('X-CSRF-TOKEN', document.querySelector('meta[name="csrf-token"]').content);
xhr.send(formData);
});
}
Chunked Upload for Large Files
For files 100 MB+ — use chunking. De-facto standard is tus protocol, for S3 — Multipart Upload API.
// tus-js-client
import { Upload } from 'tus-js-client';
const upload = new Upload(file, {
endpoint: '/api/upload/tus',
chunkSize: 5 * 1024 * 1024, // 5 MB chunks
retryDelays: [0, 1000, 3000, 5000],
metadata: { filename: file.name, filetype: file.type },
onProgress(bytesUploaded, bytesTotal) {
const pct = ((bytesUploaded / bytesTotal) * 100).toFixed(1);
console.log(`${pct}%`);
},
onSuccess() {
console.log('Done:', upload.url);
},
});
upload.start();
Laravel backend — ankurk91/laravel-tus-upload package or own implementation via tus-php.
Server Validation (Laravel)
$request->validate([
'file' => [
'required',
'file',
'max:102400', // 100 MB
'mimes:jpg,jpeg,png,pdf,docx',
function ($attribute, $value, $fail) {
$mime = mime_content_type($value->getRealPath());
$allowed = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($mime, $allowed)) {
$fail('File type not allowed.');
}
},
],
]);
Security
-
Never trust
$_FILES['type']— onlymime_content_type()orfinfo - Store files outside
public/or separate S3 bucket without public access - Deliver via signed URLs (S3 Presigned URLs) with TTL
- Limit rate limiting on upload endpoint
- Scan archives (zip bomb protection): check compression ratio
Timeframe
Basic form with drag-and-drop, progress, S3 storage — 3–4 working days. Chunked upload with resume, antivirus check, file management admin panel — 7–10 days.







