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

  1. 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)
  }
}
  1. 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
}
  1. Error recovery: Implement automatic retry logic for network issues
  2. Progress monitoring: Use RxJS subjects to broadcast upload progress to other components
  3. 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.