File upload using Laravel and Vue.js (The Right Way)
Tags: upload, laravel, vue, ajax
Published: 2020-03-08 14:09:20

# Ways to upload a file

Most articles showing how to upload a file using JavaScript actually teach how to encode the file contents in Base64 so it can be included in the JSON request. It works, but it's not as efficient as other methods. In this post I'll show how to upload a file using the multipart/FormData method using Vue.js together with axios.

## Base64 inside JSON

Advantages:

- No need to manually encode/decode your data in JSON (if using any frontend framework or client library)
- File's content is just another field in the JSON object

Disadvantages:

- Need to encode the file in Base64
- Uses more CPU, more memory, and more network bandwidth (Base64 uses 33% more space than binary)
- Little support from backend frameworks

## Multipart

Advantages:

- No need to encode the file in Base64
- Uses less CPU, less memory, and less network bandwidth
- Full support from backend frameworks

Disadvantages:

- Need to manually encode/decode your data in JSON
- File's content is separate from the JSON object

# Getting the file

In one way or another, your page will have a file input element that lets the user choose a file. Vue will complain if you try to use `v-model` on it because file inputs are readonly, so we usually add an event handler for the `change` event.

```html
<template>
    <input type="file" @change="selectFile">
</template>

<script>
    data: () => ({
        photo: null,
        description: '',
        productId: 0,
    }),

    methods: {
        selectFile(event) {
            // `files` is always an array because the file input may be in multiple mode
            this.photo = event.target.files[0];
        }
    }
</script>
```

# Sending the file

File input elements have a `files` property that is an array of instances of the `File` class. It has some metadata about the selected file and methods to read its contents. Besides that, it can be used directly as a value in a `FormData` object. The `FormData` class allows one to use JavaScript to build the same request that a plain HTML form would create. You can use a `FormData` object as the request's body when using axios, jQuery or even plain `XMLHttpRequest` objects.

The following:

```javascript
const data = new FormData();
data.append('photo', this.photo);
data.append('description', this.description);
data.append('productId', this.productId);
axios.post("/api/photo", data);
```

Is roughly the same as:

```html
<form method="POST" enctype="multipart/form-data" action="/api/photo">
    <input type="file" name="photo"/>
    <input type="text" name="description"/>
    <input type="text" name="productId">
</form>
```

If you have complex data as arrays or nested objects, you will have to convert them to JSON manually:

```javascript
const data = new FormData();
data.append('photo', this.photo);
const json = JSON.stringify({
    description: this.description,
    productId: this.productId,
});
data.append('data', json);
axios.post("/api/photo", data);
```

# Receiving the file

At the Laravel side, there is full support to handle file uploads transparently using the `Request` class. Uploaded files are fields like any other, presented by the framework as instances of the `Illuminate\Http\UploadedFile` class. From there on you can read the file's contents or store it somewhere else.

```php
public function savePhoto(Request $request)
{
    // Validate (size is in KB)
    $request->validate([
        'photo' => 'required|file|image|size:1024|dimensions:max_width=500,max_height=500',
    ]);

    // Read file contents...
    $contents = file_get_contents($request->photo->path());

    // ...or just move it somewhere else (eg: local `storage` directory or S3)
    $newPath = $request->photo->store('photos', 's3');
}
```

If you had complex data that you manually converted to JSON, you need to decode it before use:

```php
public function savePhoto(Request $request)
{
    $request['data'] = json_decode($request['data']);

    // Validate
    $request->validate([
        'data.description' => 'required|filled|size:100',
        'data.productId' => 'required|int|exists:App\Product,id'
    ]);

    // ...the rest is the same...
}
```

# References

- [axios](https://github.com/axios/axios)
- [File at MDN](https://developer.mozilla.org/en-US/docs/Web/API/File)
- [FormData at MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
- [Illuminate\Http\Request](https://laravel.com/api/6.x/Illuminate/Http/Request.html)
- [Illuminate\Http\UploadedFile](https://laravel.com/api/6.x/Illuminate/Http/UploadedFile.html)
- [Laravel file storage](https://laravel.com/docs/6.x/filesystem)
- [Laravel uploads](https://laravel.com/docs/6.x/requests#files)
- [Laravel validation](https://laravel.com/docs/6.x/validation)
- [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)