Reader Stacks

How to Submit Multipart Form Data (File Uploads) in an Angular Reactive Form

A file input's value isn't part of a reactive form's normal FormControl state — it needs to be captured separately and assembled into FormData before sending.

How to Submit Multipart Form Data (File Uploads) in an Angular Reactive Form

A file input doesn't integrate cleanly into a reactive form's normal FormControl value the way a text input does — the selected file needs to be captured from the DOM event directly and assembled into a FormData object before being sent as a multipart request.

The form setup

this.form = this.fb.group({
    title: ['', Validators.required],
    description: [''],
});

selectedFile: File | null = null;

The template

Capturing the selected file

onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    if (input.files && input.files.length > 0) {
        this.selectedFile = input.files[0];
    }
}

The file input's selected file is captured from the native DOM change event, entirely separate from the reactive form's own value — this is the fundamental reason file uploads need this extra handling step that a normal text field never does.

Assembling FormData and submitting

onSubmit(): void {
    if (this.form.invalid || !this.selectedFile) {
        return;
    }

    const formData = new FormData();
    formData.append('title', this.form.value.title);
    formData.append('description', this.form.value.description);
    formData.append('file', this.selectedFile, this.selectedFile.name);

    this.http.post('/api/uploads', formData).subscribe({
        next: () => console.log('Upload successful'),
        error: (err) => console.error('Upload failed', err),
    });
}

Sending a FormData object with Angular's HttpClient automatically sets the correct multipart/form-data content type with the proper boundary string — manually setting the Content-Type header yourself actually breaks this, since the boundary parameter needs to be generated to match the actual data being sent.

Tracking upload progress

this.http.post('/api/uploads', formData, {
    reportProgress: true,
    observe: 'events',
}).subscribe(event => {
    if (event.type === HttpEventType.UploadProgress && event.total) {
        const percentDone = Math.round((100 * event.loaded) / event.total);
        this.uploadProgress = percentDone;
    } else if (event.type === HttpEventType.Response) {
        console.log('Upload complete');
    }
});

reportProgress: true combined with observe: 'events' is what surfaces incremental upload progress events — without both options set, the request only ever emits its final completed response, with no visibility into progress along the way.

Validating file type and size before upload

onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    const file = input.files?.[0];

    if (file) {
        if (!file.type.startsWith('image/')) {
            this.fileError = 'Please select an image file';
            return;
        }
        if (file.size > 5 * 1024 * 1024) {
            this.fileError = 'File must be smaller than 5MB';
            return;
        }
        this.selectedFile = file;
        this.fileError = null;
    }
}

Checking type and size client-side gives immediate feedback before an unnecessary upload attempt — following the same principle covered for other client-side validation on this site, the server still needs to enforce these same limits independently, since this client-side check is bypassable.