How to Create a Laravel Vue.js CRUD with Image Upload

Are you looking for a simple Laravel Vue JS CRUD with image upload tutorial to get started? This beginner-friendly guide will walk you step by step through building a fully functional CRUD application with Laravel 12 and Vue 3. You’ll learn how to connect your database, set up routes, create models, build Vue components, and implement CRUD operations — including handling image upload — using Axios, Bootstrap Modal, Vuelidate, vue-loading-overlay, and SweetAlert2.

By the end of this guide, you’ll have a complete Laravel Vue JS CRUD with image upload project that you can use as a foundation for your own apps — perfect for beginners who want to master full-stack development with Laravel and Vue.js.

Let’s get started with this practical Laravel Vue JS CRUD with image upload tutorial from scratch!

Tools & Libraries We’ll Use

Step 1: Create Laravel Project

Open your Terminal/CMD, go to the project’s root folder, and then run the command below:

composer global require laravel/installer
laravel new laravel-vue-crud-image-upload

Step 2: Create Database Table

Configure your Laravel project to connect with MySQL or other databases using the .env file — the first step for any Laravel Vue JS CRUD with image upload project.

To begin developing a Laravel Vue JS CRUD with image upload, we first need a database table to store our data. Laravel’s migration system makes this easy. We’ll create a migration file to define the structure of our table — whether it’s for users, products, tasks, or any custom data you need. This is the foundation of any Laravel VueJS CRUD system.

Open your Terminal/CMD, go to the project’s root folder, and then run the command below:

php artisan make:migration create_customers_table

Update the up() function with content below

    // Laravel Vue JS CRUD with image upload - laravelcenter.com
    public function up(): void
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->string('name', 50);
            $table->tinyInteger('gender');
            $table->string('email', 100);
            $table->string('image')->nullable();
            $table->timestamps();
        });
    }

Run the migration:

php artisan migrate

Database Error Fix: Specified key was too long

If you encounter the following error during a migration (often when running php artisan migrate):

SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 1000 bytes...

This issue typically occurs when using an older version of MySQL (pre-5.7.7) or an outdated MariaDB version with Laravel’s default settings.

You need to tell the Laravel database schema builder to use a smaller default string length.

Open the file app/Providers/AppServiceProvider.php. add the following line:

use Illuminate\Support\Facades\Schema;

public function boot(): void
{
    // Add this line:
    Schema::defaultStringLength(191);
}

After saving the file, you can try running your migrations again:

php artisan migrate

Step 3: Create Model

Once the database table is ready, the next step in building a Laravel Vue JS CRUD with image upload app is to create a model. The model acts as a bridge between your database and the application logic. Laravel makes it simple to define models that interact directly with the database, allowing you to retrieve, insert, update, and delete records efficiently.

Create a file named Customer.php in the PROJECT_ROOT/app/Models directory. You can either use an artisan command or create the file manually.

<?php
// Laravel Vue JS CRUD with image upload - laravelcenter.com
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    //
}

Step 4: Create Controller with Ajax Logic

In any Laravel Vue JS CRUD with image upload workflow, the controller handles the business logic. It responds to route requests and communicates between your model and the view or Vue component. You’ll use the controller to define methods like list, submit, and delete, which are essential for performing CRUD operations.

Create a file named CustomerController.php in the PROJECT_ROOT/app/Http/Controllers directory. You can either use an artisan command or create the file manually.

<?php
// Laravel Vue JS CRUD with image upload - laravelcenter.com
namespace App\Http\Controllers;

use App\Models\Customer;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;

class CustomerController extends Controller
{
    public function list(Request $request)
    {
        try {
            $search = $request->get('search');
            $gender = $request->get('gender', 0);
            $field = $request->get('field', 'created_at');
            $order = $request->get('order', 'desc');
            $customers = Customer::when($gender > 0, function ($query) use ($gender) {
                $query->where('gender', $gender);
            })
                ->when($search, function ($query) use ($search) {
                    $query->where('name', 'like', '%' . $search . '%');
                })->orderBy($field, $order)->paginate($request->item_per_page);
            $s = true;
            $d = $customers;
        } catch (Exception $ex) {
            $s = false;
            $d = $ex->getMessage();
        }
        // response
        $response = [
            's' => $s,
            'd' => $d,
        ];

        return response()->json($response);
    }

    public function submit(Request $request)
    {
        // validation
        $validator = Validator::make($request->all(), [
            'name' => 'required|unique:customers,name,' . $request->id,
            'gender' => 'required',
            'email' => 'required|email:rfc,dns',
        ]);

        if ($validator->stopOnFirstFailure()->fails()) {
            return response()->json(
                [
                    's' => false,
                    'd' => $validator->errors()->first()
                ]
            );
        }
        try {
            // save to database
            if ($request->id > 0) {
                $data = Customer::find($request->id);
            } else {
                $data = new Customer();
            }

            $data->name = $request->name;
            $data->gender = $request->gender;
            $data->email = $request->email;

            // delete uploaded file
            if ($request->is_deleted_image == 1 && $request->id > 0) {
                if (Storage::disk('public')->exists($data->image)) {
                    Storage::disk('public')->delete($data->image);
                }
                $data->image = '';
            }
            // upload file
            else if ($request->hasFile('image')) {
                if ($data->image && Storage::disk('public')->exists($data->image)) {
                    Storage::disk('public')->delete($data->image);
                }
                $data->image = Storage::disk('public')->put('customer', $request->image);
            }

            $data->save();
            $s = true;
            $d = null;
        } catch (\Exception $ex) {
            $s = false;
            $d = $ex->getMessage();
        }

        $response = [
            's' => $s,
            'd' => $d,
        ];
        return response()->json($response);
    }

    public function delete(Request $request)
    {
        $data = Customer::find($request->delete_id);
        // delete uploaded file
        if ($data && $data->image && Storage::disk('public')->exists($data->image)) {
            Storage::disk('public')->delete($data->image);
        }
        $data->delete();
        return response()->noContent();
    }
}

Step 5: Create Blade Views with Vue Component Integration

As mentioned earlier, since we’re using Bootstrap, Vue.js, vue-loading-overlay, Vuelidate, Day.js, and SweetAlert2 on the frontend, we need to ensure all of these packages are properly installed and configured.

  • Bootstrap helps us create responsive and modern UI elements like forms and modals.
  • Vue.js powers the dynamic frontend behavior and handles data reactivity in our CRUD components.
  • vue-loading-overlay provides a smooth loading indicator when performing asynchronous actions like saving or deleting data.
  • Vuelidate is used to validate our form inputs inside Vue components in a clean and reactive way.
  • Day.js helps format and manipulate dates easily within the frontend.
  • SweetAlert2 replaces standard JavaScript alerts with beautifully designed popups for confirmations and success messages.

With all these tools combined, we can build a clean, interactive, and fully functional Laravel Vue JS CRUD with image upload application with great user experience.

Run the following command to install them via npm:

npm install vue vue-loader @vitejs/plugin-vue vue-loading-overlay
npm install bootstrap bootstrap-icons @vuelidate/core @vuelidate/validators dayjs sweetalert2

To start using Vue in your Laravel application, you need to configure vite.config.js as shown below:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ]
});

After installing the necessary packages, you need to import them into your project. Open the resources/js/app.js file and update it as follows:

import './bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-icons/font/bootstrap-icons.min.css';
import * as bootstrap from 'bootstrap';

window.bootstrap = bootstrap;

import { createApp } from 'vue'
import Customer from './Components/Customer.vue'

const app = createApp({
    created() {
        document.body.style.display = "block";
    }
})

app.component('customer', Customer)
app.mount('#app')

To make your Laravel Vue JS CRUD with image upload interactive and modern, we’ll use Blade templates to serve the base layout and embed Vue components to handle dynamic data. Vue 3 will power the front-end logic, while Bootstrap Modal helps manage the user interface for creating and editing items. This is where Vue, Axios, and Bootstrap really shine in your Laravel VueJS CRUD setup.

Create a file named laravel_vuejs_crud.blade.php in the PROJECT_ROOT/resources/views directory. You can either use an artisan command or create the file manually.

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ env('APP_NAME') }}</title>
    <style>
        .required label:after {
            content: " *";
            color: red;
            font-weight: bold;
        }
    </style>
    @vite('resources/js/app.js')
</head>

<body style="display: none;">
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-success">
        <div class="container-fluid">
            <a class="navbar-brand fw-bold" href="{{ url('/') }}">Laravel Center</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
                aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
        </div>
    </nav>
    <main style="margin-top: 100px;">
        <div id="app">
            <customer></customer>
        </div>
    </main>
</body>

</html>

Step 6: Create Vue Component

Create a file named Customer.vue in the PROJECT_ROOT/resources/js/Components directory. You can either use an artisan command or create the file manually.

<template>
    <div class="vl-parent">
        <loading v-model:active="isLoading" :is-full-page="false" />
        <div class="modal fade" ref="formModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static"
            data-bs-keyboard="false" data-bs-focus="false">
            <div class="modal-dialog modal-lg">
                <div class="modal-content">
                    <div class="modal-header py-2 bg-secondary text-light">
                        <h5 class="modal-title" style="font-weight: bold">{{ form.id > 0 ? "Edit" : "Add New" }}
                            Customer</h5>
                    </div>
                    <div class="modal-body">
                        <div class="row">
                            <div class="col-md-8">
                                <div class="row">
                                    <div class="col-12 mb-2 required">
                                        <label class="form-label">Name</label>
                                        <input type="text"
                                            :class="['form-control', { 'is-invalid': v$.form.name.$error }]"
                                            v-model="form.name" ref="autofocus" />
                                        <span v-if="v$.form.name.$error" class="invalid-feedback">
                                            {{ v$.form.name.$errors[0].$message }}
                                        </span>
                                    </div>
                                </div>
                                <div class="row">
                                    <div class="col-12 mb-2 required">
                                        <label class="form-label">Gender</label>
                                        <select v-model="form.gender"
                                            :class="v$.form.gender.$error ? 'form-select is-invalid' : 'form-select'">
                                            <option v-for="option in dd_gender" :value="option.id">
                                                {{ option.label }}
                                            </option>
                                        </select>
                                        <span v-if="v$.form.gender.$error" class="invalid-feedback">
                                            {{ v$.form.gender.$errors[0].$message }}
                                        </span>
                                    </div>
                                </div>
                                <div class="row">
                                    <div class="col-12 mb-2 required">
                                        <label class="form-label">Email</label>
                                        <input type="text"
                                            :class="['form-control', { 'is-invalid': v$.form.email.$error }]"
                                            v-model="form.email" />
                                        <span v-if="v$.form.email.$error" class="invalid-feedback">
                                            {{ v$.form.email.$errors[0].$message }}
                                        </span>
                                    </div>
                                </div>
                            </div>
                            <div class="col-md-4">
                                <div class="row">
                                    <div class="col-12 mb-2">
                                        <label class="form-label">Photo</label>
                                        <div class="text-center" style="position: relative;">
                                            <i class="bi bi-x-circle fs-3 m-0 p-0 text-danger"
                                                style="position: absolute; right: 20px;top: -20px; cursor: pointer;"
                                                @click.stop="removeImage"></i>
                                            <img :src="image_preview"
                                                style="width: 180px; height: 180px;cursor: pointer;"
                                                class="img-thumbnail" @click="upload" />
                                            <p class="mb-0 text-danger" style="font-size: 12px;" v-if="image_error">{{
                                                image_error }}</p>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-danger pt-1" data-bs-dismiss="modal">
                            <i class="bi bi-x-lg"></i> Cancel
                        </button>
                        <button type="button" class="btn btn-primary px-4" @click="saveData()">
                            <i class="bi bi-floppy" style="padding-right: 10px;"></i>Save
                        </button>
                    </div>
                </div>
            </div>
        </div>

        <div class="container">
            <div class="pagetitle">
                <a type="button" class="btn btn-primary" style="float: right" @click="addData()">
                    <i class="bi bi-plus-circle"></i> Add New
                </a>
                <h2>Customers List</h2>
                <hr />
            </div>

            <div class="row pb-3">
                <div class="col-md-10">
                    <div class="row justify-content-start">
                        <div class="col-lg-3 col-sm-6">
                            <label class="form-label mb-0">Name</label>
                            <input type="text" class="form-control" v-model="objFilter.search"
                                @keypress.enter="searchData()" placeholder="Search..." />
                        </div>
                        <div class="col-lg-3 col-sm-6">
                            <label class="form-label mb-0">Gender</label>
                            <select class="form-select" v-model="objFilter.gender">
                                <option value="0">ALL</option>
                                <option v-for="option in dd_gender" :value="option.id">
                                    {{ option.label }}
                                </option>
                            </select>
                        </div>
                    </div>
                </div>
                <div class="col-md-2 align-self-end">
                    <button type="button" class="btn btn-secondary pt-1" style="float: right" @click="searchData()">
                        <i class="bi bi-search"></i> Search
                    </button>
                </div>
            </div>
            <table class="table table-striped">
                <thead class="table-dark">
                    <tr>
                        <th width="60px" class="text-center">No</th>
                        <th width="150px" class="text-center">Photo</th>
                        <th @click="setSortBy('name')" style="cursor: pointer">
                            Name <i class="text-secondary"
                                :class="field == 'name' ? (order == 'asc' ? 'bi bi-sort-alpha-down' : 'bi bi-sort-alpha-down-alt') : 'bi bi-arrow-down-up'"></i>
                        </th>
                        <th @click="setSortBy('gender')" style="cursor: pointer;">
                            Gender <i class="text-secondary"
                                :class="field == 'gender' ? (order == 'asc' ? 'bi bi-sort-alpha-down' : 'bi bi-sort-alpha-down-alt') : 'bi bi-arrow-down-up'"></i>
                        </th>
                        <th @click="setSortBy('email')" style="cursor: pointer; padding-right: 50px;">
                            Email <i class="text-secondary"
                                :class="field == 'email' ? (order == 'asc' ? 'bi bi-sort-alpha-down' : 'bi bi-sort-alpha-down-alt') : 'bi bi-arrow-down-up'"></i>
                        </th>
                        <th @click="setSortBy('created_at')" style="cursor: pointer">
                            Created Date <i class="text-secondary"
                                :class="field == 'created_at' ? (order == 'asc' ? 'bi bi-sort-alpha-down' : 'bi bi-sort-alpha-down-alt') : 'bi bi-arrow-down-up'"></i>
                        </th>
                        <th width="150px" class="text-center">Action</th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="(data, index) in ArrayData">
                        <th style="vertical-align: middle;text-align: center">{{ tableNo + index }}</th>
                        <td class="text-center py-2"><img style="width: 60px;height: 60px;"
                                :src="data.image ? ('storage/' + data.image) : ('default.png')" />
                        </td>
                        <td style="vertical-align: middle">{{ data.name }}</td>
                        <td style="vertical-align: middle">{{ data.gender == 1 ? 'Female' : 'Male' }}</td>
                        <td style="vertical-align: middle">{{ data.email }}</td>
                        <td style="vertical-align: middle">{{ dayjs(data.created_at).format("DD/MM/YYYY HH:mm:ss") }}
                        </td>
                        <td style="vertical-align: middle; text-align: center;">
                            <i class="bi bi-trash delete-icon mx-2 text-danger fs-5" style="cursor: pointer;"
                                @click.stop="deleteData(data.id)"></i>
                            <i class="bi bi-pencil-square edit-icon mx-2 text-success fs-5" style="cursor: pointer;"
                                @click.stop="editData(data)"></i>
                        </td>
                    </tr>
                </tbody>
            </table>
            <div class="row pt-3 float-end">
                <nav>
                    <ul class="pagination">
                        <li :class="[' page-item', data.url ? '' : 'disabled', data.active ? 'active' : '']"
                            v-for="data in paginateLinks">
                            <span class="page-link" style="cursor: pointer" v-html="data.label"
                                v-if="data.url && !data.active"
                                @click="paginate(data.url.substring(data.url.lastIndexOf('?page=') + 6))"></span>
                            <span class="page-link" v-html="data.label" v-else></span>
                        </li>
                    </ul>
                </nav>
            </div>
        </div>
    </div>
</template>

<script setup>
import Loading from "vue-loading-overlay";
import "vue-loading-overlay/dist/css/index.css";
import { Modal } from "bootstrap";
import { onMounted, onUnmounted, ref } from "vue";
import { useVuelidate } from "@vuelidate/core";
import { required, email } from "@vuelidate/validators";
import Swal from "sweetalert2";
import dayjs from "dayjs";

const isLoading = ref(false);
const dd_gender = ref([
    { id: 1, label: "Female" },
    { id: 2, label: "Male" }
]);
const form = ref({
    id: 0,
    name: null,
    gender: null,
    email: null
});
const rules = {
    form: {
        name: { required },
        gender: { required },
        email: { required, email }
    }
};
const v$ = useVuelidate(rules, { form });

const objFilter = ref({
    name: null,
    gender: 0,
});

const formModal = ref(null);
const formModalInstance = ref(null);
const image_preview = ref('./default.png');
const image_error = ref(null);

const addData = () => {
    form.value = {
        id: 0,
        name: null,
        gender: null,
        email: null,
        image: null,
        is_deleted_image: 0
    };
    image_error.value = null;
    image_preview.value = 'default.png';
    formModalInstance.value.show();
};

const editData = (data) => {
    form.value = {
        id: data.id,
        name: data.name,
        gender: data.gender,
        email: data.email,
        image: null,
        is_deleted_image: 0
    };
    image_preview.value = data.image ? ('./storage/' + data.image) : 'default.png';
    formModalInstance.value.show();
};

const saveData = () => {
    v$.value.$touch();
    if (!v$.value.$invalid) {
        isLoading.value = true;
        axios.post("api/submit", form.value, {
            headers: {
                "Content-Type": "multipart/form-data",
            },
        }).then((response) => {
            const status = response.data.s;
            const data = response.data.d;
            if (status) {
                form.value.image = null;
                formModalInstance.value.hide();
                getData();
                Swal.fire("SUCCESS", "Data has been saved successfully", "success");
            }
            else {
                Swal.fire("ERROR", data, "error");
            }
        }).catch((error) => {
            Swal.fire("ERROR", error.message, "error");
        }).finally(() => {
            isLoading.value = false;
        });
    }
};

const deleteData = (id) => {
    Swal.fire({
        title: "Are you sure?",
        text: "You won't be able to revert this",
        icon: "error",
        showCancelButton: true,
        confirmButtonColor: "#3085d6",
        cancelButtonColor: "#d33",
        confirmButtonText: "Yes, delete it",
        cancelButtonText: "No, cancel",
        reverseButtons: true,
    }).then((result) => {
        if (result.isConfirmed) {
            isLoading.value = true;
            axios.delete("api/delete", { data: { delete_id: id } })
                .then(() => {
                    currentPage.value = 1;
                    getData();
                }).catch((error) => {
                    Swal.fire("ERROR", error.message, "error");
                }).finally(() => {
                    isLoading.value = false;
                });
        }
    });
};

const paginateLinks = ref([]);
const ArrayData = ref([]);
const tableNo = ref(1);
const currentPage = ref(1);
const totalPage = ref(1);
const itemPerPage = ref(5);
const field = ref(null);
const order = ref('asc');

const getData = () => {
    isLoading.value = true;
    if (!field.value)
        field.value = 'created_at';
    var param = "?page=" + currentPage.value + "&item_per_page=" + itemPerPage.value + "&field=" + field.value + "&order=" + (order.value == 'asc' ? 'desc' : 'asc');
    if (objFilter.value.search) param += "&search=" + objFilter.value.search;
    if (objFilter.value.gender)
        param += "&gender=" + objFilter.value.gender;

    axios.get("api/list" + param).then((response) => {
        const status = response.data.s;
        const data = response.data.d;
        if (status) {
            paginateLinks.value = data.links;
            ArrayData.value = data.data;
            totalPage.value = data.last_page;
            currentPage.value = data.current_page;
            tableNo.value = itemPerPage.value * (data.current_page - 1) + 1;
        }
        else {
            Swal.fire("ERROR", data, "error");
        }
    }).catch((error) => {
        Swal.fire("ERROR", error.message, "error");
    }).finally(() => {
        isLoading.value = false;
    });
};
const autofocus = ref(null);

const removeImage = () => {
    form.value.is_deleted_image = 1;
    form.value.image = null;
    image_preview.value = 'default.png';
    image_error.value = null;
};

const upload = () => {
    let acceptFileType = ['image/png', 'image/jpg', 'image/jpeg'];
    let input = document.createElement('input');
    input.type = 'file';
    input.accept = '.png,.jpg,.jpeg';
    input.onchange = _ => {
        let file = input.files[0];
        if (!acceptFileType.includes(file.type.toLocaleLowerCase())) {
            image_error.valueOf = 'Accept file type: png, jpg, jpeg';
            return;
        } else if (file.size > 2097152) {
            image_error.value = 'File size must be less than 2mb';
            return;
        }
        image_preview.value = URL.createObjectURL(file);
        form.value.image = file;
    };
    input.click();
};

onMounted(() => {
    if (formModal.value) {
        formModalInstance.value = new Modal(formModal.value);
        formModal.value.addEventListener("shown.bs.modal", () => {
            autofocus.value.focus();
        });
        formModal.value.addEventListener("hide.bs.modal", () => {
            document.activeElement?.blur();
            v$.value.$reset();
        });
    }
    getData();
});
onUnmounted(() => {
    if (formModalInstance.value) {
        formModalInstance.value.dispose();
    }
});

const searchData = () => {
    currentPage.value = 1;
    getData();
};

const setSortBy = (column_name) => {
    currentPage.value = 1;
    field.value = column_name;
    order.value = (order.value == 'asc' ? 'desc' : 'asc');
    getData();
};

const paginate = (page_number) => {
    currentPage.value = page_number;
    if (page_number > totalPage.value) {
        currentPage.valueOf = totalPage.value;
    }
    if (page_number <= 0) {
        currentPage.value = 1;
    }
    getData();
};
</script>

Step 7: Define Routes

Finally, for a complete Laravel Vue JS CRUD with image upload application, we need to define routes in web.php or api.php. Routes connect your controller actions to specific URLs. Laravel’s powerful routing system ensures that each CRUD operation (Create, Read, Update, Delete) is accessible via a dedicated endpoint.

In routes/web.php:

<?php

use Illuminate\Support\Facades\Route;

Route::view('/', 'laravel_vuejs_crud');

Laravel 12 doesn’t include api.php by default, so run:

php artisan install:api

In routes/api.php:

<?php

use App\Http\Controllers\CustomerController;
use Illuminate\Support\Facades\Route;

Route::controller(CustomerController::class)->group(function () {
    Route::get('list', 'list');
    Route::post('submit', 'submit');
    Route::delete('delete', 'delete');
});

Additional Setup

💡 Tip: To ensure the project loads smoothly without broken images, you can download this default image and place it inside your Laravel project’s public/ folder. This helps prevent errors when no uploaded images are available yet.

Fixing Pagination Styling with Bootstrap in Laravel

By default, Laravel uses Tailwind CSS for pagination views. If your project is using Bootstrap (especially Bootstrap 4 or 5), the pagination links generated by {{ $items->links() }} may look unstyled or broken.

To fix this, you need to tell Laravel to use Bootstrap-compatible pagination views. You can do this by updating the AppServiceProvider.

Open the file:
app/Providers/AppServiceProvider.php

Inside the boot() method, add the following line:

use Illuminate\Pagination\Paginator;

public function boot()
{
    Paginator::useBootstrap();
}

This tells Laravel to render pagination links using Bootstrap’s markup instead of Tailwind CSS.

When you’re handling image uploads in Laravel, the uploaded files are typically stored in the storage/app/public directory. However, these files need to be publicly accessible from the browser. Laravel provides a convenient command to create a symbolic link between the public/storage directory and storage/app/public:

php artisan storage:link

This command will generate a symbolic link so you can access uploaded images using a public URL like /storage/your-image.jpg.

Running and Testing Your Project

Open your Terminal/CMD in separate windows, go to the project’s root folder, and then run the command below:

npm run dev
php artisan serve

With both commands running in their separate windows, open your web browser to the Laravel address (http://127.0.0.1:8000). You should now see the customer list with image upload, search, sort, and pagination features working as expected.

By following this guide, you now have a solid understanding of building a full-featured Laravel Vue JS CRUD with image upload , search, sort, and pagination.

Conclusion

You’ve successfully learned how to build a complete Laravel Vue JS CRUD with image upload feature in this easy step-by-step guide. By combining Laravel’s backend power with Vue.js’s reactive frontend, you can create a modern, dynamic application that handles create, read, update, and delete operations seamlessly — along with secure image uploads.

This Laravel Vue JS CRUD with image upload approach not only improves performance but also enhances user experience with real-time updates and cleaner workflows. You can extend this setup by adding validation, pagination, or drag-and-drop image uploads to make it even more robust.

Mastering Laravel Vue JS CRUD with image upload is a solid foundation for building interactive, scalable, and user-friendly web applications.

Senghok
Senghok

Senghok is a web developer who enjoys working with Laravel and Vue.js. He creates easy-to-follow tutorials and guides to help beginners learn step by step. His goal is to make learning web development simple and fun for everyone.

Articles: 44

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *