Resumable file uploads in Angular

In today's web applications, unreliable networks and large files can lead to frustrating user experiences during file uploads. Resumable uploads allow your users to continue their upload where they left off if a disruption occurs. In this post, we show you how to integrate the tus-js-client into your Angular app to create robust, resumable file uploads.
Why resumable uploads?
Resumable uploads tackle common issues with large files and unstable networks. By breaking a file into manageable chunks and allowing failed segments to be retried, you ensure a more reliable user experience. The standardized TUS protocol, which tus-js-client implements, makes this process easier to manage and scale.
Setting up your Angular project
Start by creating a new Angular project using the Angular CLI with standalone components:
npm install -g @angular/cli
ng new resumable-upload-demo --standalone
cd resumable-upload-demo
Install the tus-js-client package with TypeScript types:
npm install tus-js-client@4.3.1
Creating an upload service
Let's create an Angular service to handle uploads using tus-js-client. This service includes proper Typescript types and comprehensive error handling:
import { Injectable } from '@angular/core'
import { Upload, UploadOptions } from 'tus-js-client'
export interface UploadProgress {
bytesUploaded: number
bytesTotal: number
percentage: number
}
@Injectable({
providedIn: 'root',
})
export class UploadService {
private upload: Upload | null = null
startUpload(
file: File,
endpoint: string,
onProgress: (progress: UploadProgress) => void,
onSuccess: () => void,
onError: (error: Error) => void,
): void {
const options: UploadOptions = {
endpoint,
retryDelays: [0, 1000, 3000, 5000],
chunkSize: 5 * 1024 * 1024, // 5MB chunks for optimal performance
metadata: {
filename: file.name,
filetype: file.type,
},
onError: (error) => {
if (error.name === 'AbortError') {
onError(new Error('Upload was aborted'))
} else if (error.name === 'NetworkError') {
onError(new Error('Network connection lost - upload will resume automatically'))
} else {
onError(error)
}
},
onProgress: (bytesUploaded: number, bytesTotal: number) => {
onProgress({
bytesUploaded,
bytesTotal,
percentage: (bytesUploaded / bytesTotal) * 100,
})
},
onSuccess: () => {
onSuccess()
},
}
this.upload = new Upload(file, options)
this.upload.start()
}
abortUpload(): void {
if (this.upload) {
this.upload.abort()
}
}
}
Building the upload component
Create a standalone component that manages file selection and upload progress:
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { UploadService, UploadProgress } from './upload.service'
@Component({
selector: 'app-upload',
standalone: true,
imports: [CommonModule],
template: `
<div class="upload-container">
<input
type="file"
(change)="onFileSelected($event)"
[attr.aria-label]="'Choose file to upload'"
/>
<button
(click)="startUpload()"
[disabled]="!selectedFile || isUploading"
class="upload-button"
>
{{ isUploading ? 'Uploading...' : 'Upload' }}
</button>
<div *ngIf="progress" class="progress-container">
<div class="progress-bar" [style.width.%]="progress.percentage">
{{ progress.percentage | number: '1.0-0' }}%
</div>
</div>
<div *ngIf="message" [class]="messageType" role="alert">
{{ message }}
</div>
</div>
`,
styles: [
`
.upload-container {
padding: 1rem;
}
.progress-container {
margin-top: 1rem;
background: #f0f0f0;
border-radius: 4px;
}
.progress-bar {
height: 20px;
background: #4caf50;
border-radius: 4px;
text-align: center;
color: white;
transition: width 0.3s ease;
}
.error {
color: #d32f2f;
margin-top: 1rem;
}
.success {
color: #388e3c;
margin-top: 1rem;
}
`,
],
})
export class UploadComponent {
selectedFile: File | null = null
progress: UploadProgress | null = null
message = ''
messageType = ''
isUploading = false
constructor(private uploadService: UploadService) {}
onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement
if (input.files && input.files.length > 0) {
this.selectedFile = input.files[0]
this.progress = null
this.message = ''
this.messageType = ''
}
}
startUpload(): void {
if (!this.selectedFile) return
this.isUploading = true
const uploadEndpoint = 'https://master.tus.io/files/'
this.uploadService.startUpload(
this.selectedFile,
uploadEndpoint,
(progress) => {
this.progress = progress
},
() => {
this.message = 'Upload completed successfully!'
this.messageType = 'success'
this.isUploading = false
},
(error: Error) => {
this.message = error.message
this.messageType = 'error'
this.isUploading = false
},
)
}
}
Cors configuration
Ensure your tus server includes the following CORS headers for proper operation:
// Required CORS headers
Access-Control-Allow-Origin: <your-domain>
Access-Control-Allow-Methods: POST, PATCH, HEAD, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset
Access-Control-Expose-Headers: Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata
Best practices and considerations
- Authentication: Integrate with Angular's HttpInterceptors for secure uploads:
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http'
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler) {
const authToken = 'your-auth-token'
const modifiedRequest = request.clone({
headers: request.headers.set('Authorization', `Bearer ${authToken}`),
})
return next.handle(modifiedRequest)
}
}
- File validation: Implement proper file type and size checks:
validateFile(file: File): boolean {
const maxSize = 100 * 1024 * 1024 // 100MB
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
if (file.size > maxSize) {
this.message = 'File size exceeds 100MB limit'
return false
}
if (!allowedTypes.includes(file.type)) {
this.message = 'Invalid file type'
return false
}
return true
}
- Error recovery: Implement automatic retry logic for network issues
- Progress monitoring: Use RxJS subjects to broadcast upload progress to other components
- Cleanup: Properly handle component destruction and abort ongoing uploads
Conclusion
Implementing resumable uploads in Angular with tus-js-client provides a robust solution for handling large files and network interruptions. The combination of TypeScript types, proper error handling, and best practices ensures a reliable upload experience for your users.
If you need a more comprehensive file handling solution, consider exploring Transloadit's API, which offers advanced features for file processing and transformation.