- Published on
Complete Guide to File Upload in Frontend Development
Complete Guide to File Upload in Frontend Development
File upload is a fundamental feature in modern web applications, from profile pictures to document management systems. This comprehensive guide covers everything you need to know about implementing robust, secure, and user-friendly file upload functionality in frontend applications.
Table of Contents
- File Upload Fundamentals
- Basic Implementation
- Advanced Upload Techniques
- Security Considerations
- Performance Optimization
- Framework Integration
- Error Handling
- Best Practices
File Upload Fundamentals
Understanding File Objects
// File object properties
const file = document.getElementById('fileInput').files[0]
console.log({
name: file.name, // 'document.pdf'
size: file.size, // 1024000 (bytes)
type: file.type, // 'application/pdf'
lastModified: file.lastModified, // 1640995200000
lastModifiedDate: new Date(file.lastModified),
})
File Input Types
<!-- Single file upload -->
<input type="file" id="singleFile" accept=".jpg,.png,.gif" />
<!-- Multiple file upload -->
<input type="file" id="multipleFiles" multiple accept="image/*" />
<!-- Directory upload -->
<input type="file" id="directoryUpload" webkitdirectory />
<!-- Capture from camera -->
<input type="file" accept="image/*" capture="environment" />
Basic Implementation
Simple File Upload
class FileUploader {
constructor(options = {}) {
this.apiEndpoint = options.apiEndpoint || '/api/upload'
this.allowedTypes = options.allowedTypes || ['image/jpeg', 'image/png']
this.maxSize = options.maxSize || 5 * 1024 * 1024 // 5MB
this.onProgress = options.onProgress || (() => {})
this.onSuccess = options.onSuccess || (() => {})
this.onError = options.onError || (() => {})
}
async upload(file) {
// Validate file
if (!this.validateFile(file)) {
return
}
const formData = new FormData()
formData.append('file', file)
formData.append('timestamp', Date.now())
try {
const response = await this.uploadWithProgress(formData)
this.onSuccess(response)
return response
} catch (error) {
this.onError(error)
throw error
}
}
validateFile(file) {
if (!this.allowedTypes.includes(file.type)) {
this.onError(new Error(`File type ${file.type} not allowed`))
return false
}
if (file.size > this.maxSize) {
this.onError(new Error(`File size exceeds ${this.maxSize} bytes`))
return false
}
return true
}
uploadWithProgress(formData) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const progress = (e.loaded / e.total) * 100
this.onProgress(progress)
}
})
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(`Upload failed: ${xhr.status}`))
}
})
xhr.addEventListener('error', () => {
reject(new Error('Upload failed'))
})
xhr.open('POST', this.apiEndpoint)
xhr.send(formData)
})
}
}
// Usage
const uploader = new FileUploader({
apiEndpoint: '/api/upload',
allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],
maxSize: 10 * 1024 * 1024, // 10MB
onProgress: (progress) => {
console.log(`Upload progress: ${progress.toFixed(2)}%`)
},
onSuccess: (response) => {
console.log('Upload successful:', response)
},
onError: (error) => {
console.error('Upload error:', error.message)
},
})
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0]
if (file) {
uploader.upload(file)
}
})
Multiple File Upload
class MultiFileUploader {
constructor(options = {}) {
this.options = options
this.queue = []
this.concurrent = options.concurrent || 3
this.activeUploads = 0
}
async uploadMultiple(files) {
const fileArray = Array.from(files)
this.queue = fileArray.map((file) => ({
file,
status: 'pending',
progress: 0,
id: this.generateId(),
}))
this.processQueue()
return this.waitForCompletion()
}
async processQueue() {
while (this.queue.length > 0 && this.activeUploads < this.concurrent) {
const item = this.queue.find((item) => item.status === 'pending')
if (!item) break
item.status = 'uploading'
this.activeUploads++
try {
const result = await this.uploadSingle(item)
item.status = 'completed'
item.result = result
} catch (error) {
item.status = 'failed'
item.error = error
}
this.activeUploads--
this.processQueue() // Continue processing
}
}
async uploadSingle(item) {
const formData = new FormData()
formData.append('file', item.file)
formData.append('fileId', item.id)
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
item.progress = (e.loaded / e.total) * 100
this.updateProgress()
}
})
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(`Upload failed: ${xhr.status}`))
}
})
xhr.addEventListener('error', () => {
reject(new Error('Network error'))
})
xhr.open('POST', this.options.apiEndpoint || '/api/upload')
xhr.send(formData)
})
}
updateProgress() {
const totalProgress = this.queue.reduce((sum, item) => sum + item.progress, 0)
const averageProgress = totalProgress / this.queue.length
if (this.options.onProgress) {
this.options.onProgress(averageProgress, this.queue)
}
}
waitForCompletion() {
return new Promise((resolve) => {
const checkCompletion = () => {
const allDone = this.queue.every(
(item) => item.status === 'completed' || item.status === 'failed'
)
if (allDone) {
resolve(this.queue)
} else {
setTimeout(checkCompletion, 100)
}
}
checkCompletion()
})
}
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
}
Advanced Upload Techniques
Drag and Drop Upload
class DragDropUploader {
constructor(dropZone, options = {}) {
this.dropZone = dropZone
this.options = options
this.setupEventListeners()
}
setupEventListeners() {
const events = ['dragenter', 'dragover', 'dragleave', 'drop']
events.forEach((eventName) => {
this.dropZone.addEventListener(eventName, this.preventDefaults, false)
})
this.dropZone.addEventListener('dragenter', this.handleDragEnter.bind(this))
this.dropZone.addEventListener('dragover', this.handleDragOver.bind(this))
this.dropZone.addEventListener('dragleave', this.handleDragLeave.bind(this))
this.dropZone.addEventListener('drop', this.handleDrop.bind(this))
}
preventDefaults(e) {
e.preventDefault()
e.stopPropagation()
}
handleDragEnter(e) {
this.dropZone.classList.add('drag-over')
}
handleDragOver(e) {
e.dataTransfer.dropEffect = 'copy'
}
handleDragLeave(e) {
if (!this.dropZone.contains(e.relatedTarget)) {
this.dropZone.classList.remove('drag-over')
}
}
handleDrop(e) {
this.dropZone.classList.remove('drag-over')
const files = Array.from(e.dataTransfer.files)
const items = Array.from(e.dataTransfer.items)
// Handle directory uploads
if (items.length > 0 && items[0].webkitGetAsEntry) {
this.handleDirectoryUpload(items)
} else {
this.handleFileUpload(files)
}
}
async handleDirectoryUpload(items) {
const files = []
for (const item of items) {
const entry = item.webkitGetAsEntry()
if (entry) {
const entryFiles = await this.traverseDirectory(entry)
files.push(...entryFiles)
}
}
this.handleFileUpload(files)
}
traverseDirectory(entry, path = '') {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file((file) => {
file.relativePath = path + file.name
resolve([file])
})
} else if (entry.isDirectory) {
const dirReader = entry.createReader()
const files = []
const readEntries = () => {
dirReader.readEntries(async (entries) => {
if (entries.length === 0) {
resolve(files)
} else {
for (const childEntry of entries) {
const childFiles = await this.traverseDirectory(childEntry, path + entry.name + '/')
files.push(...childFiles)
}
readEntries()
}
})
}
readEntries()
}
})
}
handleFileUpload(files) {
if (this.options.onFilesSelected) {
this.options.onFilesSelected(files)
}
}
}
// Usage
const dropZone = document.getElementById('dropZone')
const dragDropUploader = new DragDropUploader(dropZone, {
onFilesSelected: (files) => {
console.log('Files selected:', files)
// Process files here
},
})
Chunked Upload for Large Files
class ChunkedUploader {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 1024 * 1024 // 1MB
this.maxRetries = options.maxRetries || 3
this.apiEndpoint = options.apiEndpoint || '/api/upload'
}
async uploadFile(file) {
const totalChunks = Math.ceil(file.size / this.chunkSize)
const uploadId = this.generateUploadId()
console.log(`Starting chunked upload: ${totalChunks} chunks`)
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
await this.uploadChunk(file, chunkIndex, totalChunks, uploadId)
}
return this.finalizeUpload(uploadId, file.name)
}
async uploadChunk(file, chunkIndex, totalChunks, uploadId) {
const start = chunkIndex * this.chunkSize
const end = Math.min(start + this.chunkSize, file.size)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('chunkIndex', chunkIndex)
formData.append('totalChunks', totalChunks)
formData.append('uploadId', uploadId)
formData.append('fileName', file.name)
let retries = 0
while (retries < this.maxRetries) {
try {
const response = await fetch(`${this.apiEndpoint}/chunk`, {
method: 'POST',
body: formData,
})
if (response.ok) {
console.log(`Chunk ${chunkIndex + 1}/${totalChunks} uploaded`)
return
}
throw new Error(`HTTP ${response.status}`)
} catch (error) {
retries++
if (retries === this.maxRetries) {
throw new Error(`Failed to upload chunk ${chunkIndex} after ${this.maxRetries} retries`)
}
// Exponential backoff
await this.delay(1000 * Math.pow(2, retries - 1))
}
}
}
async finalizeUpload(uploadId, fileName) {
const response = await fetch(`${this.apiEndpoint}/finalize`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
uploadId,
fileName,
}),
})
if (!response.ok) {
throw new Error(`Failed to finalize upload: ${response.status}`)
}
return response.json()
}
generateUploadId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
}
Security Considerations
File Type Validation
class FileValidator {
static validateFileType(file, allowedTypes) {
// Check MIME type
if (!allowedTypes.includes(file.type)) {
return { valid: false, error: 'File type not allowed' }
}
// Check file extension
const extension = file.name.split('.').pop().toLowerCase()
const allowedExtensions = allowedTypes
.map((type) => {
const extensionMap = {
'image/jpeg': ['jpg', 'jpeg'],
'image/png': ['png'],
'image/gif': ['gif'],
'application/pdf': ['pdf'],
'text/plain': ['txt'],
}
return extensionMap[type] || []
})
.flat()
if (!allowedExtensions.includes(extension)) {
return { valid: false, error: 'File extension not allowed' }
}
return { valid: true }
}
static async validateFileSignature(file) {
const signatures = {
'image/jpeg': [0xff, 0xd8, 0xff],
'image/png': [0x89, 0x50, 0x4e, 0x47],
'image/gif': [0x47, 0x49, 0x46],
'application/pdf': [0x25, 0x50, 0x44, 0x46],
}
const signature = signatures[file.type]
if (!signature) {
return { valid: true } // No signature to check
}
const buffer = await file.slice(0, signature.length).arrayBuffer()
const bytes = new Uint8Array(buffer)
const matches = signature.every((byte, index) => bytes[index] === byte)
return {
valid: matches,
error: matches ? null : 'File signature does not match type',
}
}
static sanitizeFileName(fileName) {
return fileName
.replace(/[^a-zA-Z0-9.-]/g, '_')
.replace(/_{2,}/g, '_')
.replace(/^_|_$/g, '')
.substring(0, 255)
}
}
Malware Scanning
class MalwareScanner {
static async scanFile(file) {
// Client-side heuristics (basic checks)
const suspiciousExtensions = ['exe', 'bat', 'cmd', 'com', 'pif', 'scr', 'vbs', 'js']
const extension = file.name.split('.').pop().toLowerCase()
if (suspiciousExtensions.includes(extension)) {
return { safe: false, reason: 'Potentially dangerous file type' }
}
// Check for embedded scripts in images
if (file.type.startsWith('image/')) {
const result = await this.checkImageForScripts(file)
if (!result.safe) {
return result
}
}
return { safe: true }
}
static async checkImageForScripts(file) {
const text = await file.text()
const scriptPatterns = [/<script/i, /javascript:/i, /vbscript:/i, /onload=/i, /onerror=/i]
for (const pattern of scriptPatterns) {
if (pattern.test(text)) {
return { safe: false, reason: 'Script detected in image file' }
}
}
return { safe: true }
}
}
Performance Optimization
Image Compression
class ImageCompressor {
static async compressImage(file, options = {}) {
const { maxWidth = 1920, maxHeight = 1080, quality = 0.8, format = 'image/jpeg' } = options
return new Promise((resolve) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
img.onload = () => {
// Calculate new dimensions
const { width, height } = this.calculateDimensions(
img.width,
img.height,
maxWidth,
maxHeight
)
canvas.width = width
canvas.height = height
// Draw and compress
ctx.drawImage(img, 0, 0, width, height)
canvas.toBlob(resolve, format, quality)
}
img.src = URL.createObjectURL(file)
})
}
static calculateDimensions(width, height, maxWidth, maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height)
if (ratio < 1) {
return {
width: Math.floor(width * ratio),
height: Math.floor(height * ratio),
}
}
return { width, height }
}
}
Upload Queue Management
class UploadQueue {
constructor(options = {}) {
this.concurrency = options.concurrency || 3
this.queue = []
this.active = []
this.completed = []
this.failed = []
}
add(uploadTask) {
const task = {
id: this.generateId(),
...uploadTask,
status: 'queued',
progress: 0,
retries: 0,
}
this.queue.push(task)
this.processQueue()
return task.id
}
async processQueue() {
while (this.queue.length > 0 && this.active.length < this.concurrency) {
const task = this.queue.shift()
this.active.push(task)
this.executeTask(task)
}
}
async executeTask(task) {
task.status = 'uploading'
try {
const result = await task.upload()
task.status = 'completed'
task.result = result
this.moveToCompleted(task)
} catch (error) {
task.error = error
if (task.retries < 3) {
task.retries++
task.status = 'retrying'
// Add back to queue for retry
setTimeout(() => {
this.queue.unshift(task)
this.processQueue()
}, 1000 * Math.pow(2, task.retries))
} else {
task.status = 'failed'
this.moveToFailed(task)
}
}
}
moveToCompleted(task) {
this.active = this.active.filter((t) => t.id !== task.id)
this.completed.push(task)
this.processQueue()
}
moveToFailed(task) {
this.active = this.active.filter((t) => t.id !== task.id)
this.failed.push(task)
this.processQueue()
}
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
getStatus() {
return {
queued: this.queue.length,
active: this.active.length,
completed: this.completed.length,
failed: this.failed.length,
}
}
}
Framework Integration
React Hook
import { useState, useCallback, useRef } from 'react'
export const useFileUpload = (options = {}) => {
const [files, setFiles] = useState([])
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const [error, setError] = useState(null)
const uploaderRef = useRef(null)
const uploadFiles = useCallback(
async (fileList) => {
setUploading(true)
setError(null)
setProgress(0)
try {
const uploader = new MultiFileUploader({
...options,
onProgress: (progress) => setProgress(progress),
})
const results = await uploader.uploadMultiple(fileList)
setFiles(results)
return results
} catch (err) {
setError(err.message)
throw err
} finally {
setUploading(false)
}
},
[options]
)
const removeFile = useCallback((fileId) => {
setFiles((prev) => prev.filter((f) => f.id !== fileId))
}, [])
const reset = useCallback(() => {
setFiles([])
setProgress(0)
setError(null)
setUploading(false)
}, [])
return {
files,
uploading,
progress,
error,
uploadFiles,
removeFile,
reset,
}
}
// Usage in component
const FileUploadComponent = () => {
const { files, uploading, progress, error, uploadFiles } = useFileUpload({
apiEndpoint: '/api/upload',
maxSize: 10 * 1024 * 1024,
})
const handleFileChange = (e) => {
const selectedFiles = Array.from(e.target.files)
uploadFiles(selectedFiles)
}
return (
<div>
<input type="file" multiple onChange={handleFileChange} />
{uploading && <div>Upload Progress: {progress.toFixed(2)}%</div>}
{error && <div>Error: {error}</div>}
{files.map((file) => (
<div key={file.id}>
{file.file.name} - {file.status}
</div>
))}
</div>
)
}
Vue 3 Composition API
import { ref, computed } from 'vue'
export function useFileUpload(options = {}) {
const files = ref([])
const uploading = ref(false)
const progress = ref(0)
const error = ref(null)
const uploadFiles = async (fileList) => {
uploading.value = true
error.value = null
progress.value = 0
try {
const uploader = new MultiFileUploader({
...options,
onProgress: (prog) => (progress.value = prog),
})
const results = await uploader.uploadMultiple(fileList)
files.value = results
return results
} catch (err) {
error.value = err.message
throw err
} finally {
uploading.value = false
}
}
const removeFile = (fileId) => {
files.value = files.value.filter((f) => f.id !== fileId)
}
const completedFiles = computed(() => files.value.filter((f) => f.status === 'completed'))
const failedFiles = computed(() => files.value.filter((f) => f.status === 'failed'))
return {
files,
uploading,
progress,
error,
completedFiles,
failedFiles,
uploadFiles,
removeFile,
}
}
Best Practices
1. User Experience
// Show file preview
const createFilePreview = (file) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => {
const preview = document.createElement('img')
preview.src = e.target.result
preview.style.maxWidth = '200px'
document.getElementById('preview').appendChild(preview)
}
reader.readAsDataURL(file)
}
}
// Progress indication
const showProgress = (progress) => {
const progressBar = document.getElementById('progressBar')
progressBar.style.width = `${progress}%`
progressBar.textContent = `${Math.round(progress)}%`
}
// Error handling with user feedback
const handleUploadError = (error) => {
const errorMap = {
'File too large': 'Please select a smaller file (max 10MB)',
'Invalid file type': 'Please select a valid image file',
'Network error': 'Please check your connection and try again',
}
const userMessage = errorMap[error.message] || 'Upload failed. Please try again.'
showNotification(userMessage, 'error')
}
2. Performance Tips
// Lazy loading for large file lists
const LazyFileList = {
itemHeight: 50,
visibleItems: 10,
render(files, container) {
const startIndex = Math.floor(container.scrollTop / this.itemHeight)
const endIndex = Math.min(startIndex + this.visibleItems, files.length)
container.innerHTML = ''
for (let i = startIndex; i < endIndex; i++) {
const item = this.createFileItem(files[i])
item.style.top = `${i * this.itemHeight}px`
container.appendChild(item)
}
},
}
// Memory management
const cleanupFileReferences = (files) => {
files.forEach((file) => {
if (file.preview) {
URL.revokeObjectURL(file.preview)
}
})
}
Conclusion
File upload functionality is crucial for modern web applications. By implementing proper validation, security measures, and performance optimizations, you can create robust and user-friendly upload experiences.
Key Takeaways:
- Validate files on both client and server side
- Use chunked uploads for large files
- Implement proper error handling and retry logic
- Optimize performance with compression and lazy loading
- Secure your uploads with type validation and scanning
- Provide great UX with progress indicators and previews
Ready to implement file uploads in your application? Start with the basic examples and gradually add advanced features as needed.