Reader Stacks

AJAX Image Upload in Laravel

The form itself still needs enctype=multipart/form-data even though it's never actually submitted the normal way — FormData replicates that same multipart encoding manually for the AJAX request instead.

Uploading an image via AJAX rather than a full page submission follows the same underlying validation and storage logic as a normal form upload — the difference is entirely in how the file reaches the server, using FormData instead of a standard form POST.

The form markup

@csrf

The form still needs enctype="multipart/form-data" even though it's never actually submitted through the browser's normal mechanism — FormData, built below, replicates that same multipart encoding manually for the AJAX request.

The AJAX submission

$('#upload-form').submit(function (event) {
    event.preventDefault();

    const formData = new FormData(this);

    $.ajax({
        url: '/products/upload-image',
        method: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function (response) {
            $('#preview-container').html(``);
        },
        error: function (xhr) {
            alert(xhr.responseJSON.errors.image[0]);
        }
    });
});

processData: false and contentType: false are both required for a file upload via jQuery's $.ajax() — without them, jQuery attempts to serialize the FormData object as a URL-encoded string, which strips out the actual binary file data entirely.

Building FormData directly from the form element

new FormData(this), passing the form element itself, automatically collects every named field in the form — including the file input — without needing to manually append each field individually, which is simpler than building the FormData object field by field.

The Laravel controller

public function uploadImage(Request $request)
{
    $request->validate([
        'image' => 'required|image|mimes:jpeg,png,jpg|max:2048',
    ]);

    $path = $request->file('image')->store('products', 'public');

    return response()->json(['url' => asset('storage/'.$path)]);
}

Showing upload progress

$.ajax({
    url: '/products/upload-image',
    method: 'POST',
    data: formData,
    processData: false,
    contentType: false,
    xhr: function () {
        const xhr = new window.XMLHttpRequest();
        xhr.upload.addEventListener('progress', function (e) {
            if (e.lengthComputable) {
                const percent = Math.round((e.loaded / e.total) * 100);
                $('#upload-progress').css('width', percent + '%').text(percent + '%');
            }
        });
        return xhr;
    },
    success: function (response) {
        $('#preview-container').html(``);
    }
});

Overriding the xhr option to attach a progress listener on the underlying XMLHttpRequest object is necessary since jQuery's own $.ajax() doesn't expose upload progress through its standard callbacks — genuinely useful feedback for a larger image file that takes a visible amount of time to upload.

Validating file type client-side before even attempting the upload

$('#image-input').change(function () {
    const file = this.files[0];
    if (file && !['image/jpeg', 'image/png'].includes(file.type)) {
        alert('Only JPEG and PNG images are allowed.');
        this.value = '';
    }
});

This is a convenience check only — the server-side mimes:jpeg,png,jpg validation rule remains the actual enforced restriction, since a client-side check can always be bypassed by a request crafted outside the browser.