Lessons → Laravel with Vue.js

Laravel with Vue.js

Laravel
⏱ 30 min read📖 IntermediateNot completed

Laravel and Vue.js are a powerful combination — Laravel handles the backend (API, database, auth) while Vue.js creates dynamic, reactive frontend interfaces without full page reloads.

Why Laravel + Vue.js?

Laravel doesVue.js does
API routes & controllersDynamic UI without page reload
Database queriesReactive data binding
AuthenticationComponent-based UI
File uploadsReal-time updates
Business logicForm validation feedback

Step 1: Install Vue.js in Laravel

Terminal
# Install Vue and Vite plugin
npm install vue@3 @vitejs/plugin-vue

# Install axios for API calls
npm install axios
vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/js/app.js'],
            refresh: true,
        }),
        vue(),
    ],
    resolve: {
        alias: { vue: 'vue/dist/vue.esm-bundler.js' }
    },
});

Step 2: Setup Vue App

resources/js/app.js
import { createApp } from 'vue';
import axios from 'axios';

// Set CSRF token for all axios requests
axios.defaults.headers.common['X-CSRF-TOKEN'] =
    document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');

// Import components
import ProductList from './components/ProductList.vue';
import SearchBox  from './components/SearchBox.vue';
import Counter    from './components/Counter.vue';

// Mount components to DOM elements
const app = createApp({});
app.component('product-list', ProductList);
app.component('search-box', SearchBox);
app.component('counter', Counter);
app.mount('#app');

Step 3: Simple Vue Component

resources/js/components/Counter.vue
<template>
  <div class="counter">
    <button @click="decrement" :disabled="count === 0">−</button>
    <span class="count">{{ count }}</span>
    <button @click="increment">+</button>
    <p>You clicked {{ count }} time{{ count !== 1 ? 's' : '' }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(0);
const increment = () => count.value++;
const decrement = () => count.value--;
</script>

<style scoped>
.counter { display: flex; align-items: center; gap: 1rem; }
.count   { font-size: 2rem; font-weight: bold; min-width: 3rem; text-align: center; }
button   { padding: 0.5rem 1rem; font-size: 1.5rem; cursor: pointer; }
</style>

Step 4: Vue Component with Laravel API

resources/js/components/ProductList.vue
<template>
  <div>
    <!-- Search -->
    <input v-model="search" @input="fetchProducts"
           placeholder="Search products..." class="search-input" />

    <!-- Loading -->
    <div v-if="loading">Loading...</div>

    <!-- Products grid -->
    <div v-else class="products-grid">
      <div v-for="product in products" :key="product.id" class="product-card">
        <img :src="product.image_url" :alt="product.name" />
        <h3>{{ product.name }}</h3>
        <p>{{ product.formatted_price }}</p>
        <button @click="addToCart(product)" :disabled="product.stock === 0">
          {{ product.stock > 0 ? 'Add to Cart' : 'Out of Stock' }}
        </button>
      </div>
    </div>

    <!-- Empty state -->
    <p v-if="!loading && products.length === 0">No products found.</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';

const products = ref([]);
const loading  = ref(false);
const search   = ref('');

async function fetchProducts() {
    loading.value = true;
    try {
        const response = await axios.get('/api/products', {
            params: { search: search.value }
        });
        products.value = response.data.data;
    } catch (error) {
        console.error('Failed to fetch products:', error);
    } finally {
        loading.value = false;
    }
}

function addToCart(product) {
    // Emit event to parent or call API
    axios.post('/api/cart', { product_id: product.id })
        .then(() => alert(`${product.name} added to cart!`));
}

onMounted(fetchProducts); // Load on page load
</script>

Step 5: Laravel API for Vue

routes/api.php
<?php
Route::get('/products', function (Request $request) {
    return ProductResource::collection(
        Product::active()
            ->when($request->search, fn($q) =>
                $q->where('name', 'like', "%{$request->search}%"))
            ->paginate(12)
    );
});
?>

Step 6: Use in Blade Template

resources/views/products/index.blade.php
<!-- Add to head -->
<meta name="csrf-token" content="{{ csrf_token() }}" />
@vite(['resources/js/app.js'])

<!-- Mount Vue app -->
<div id="app">
    <product-list></product-list>
    <counter></counter>
</div>

<!-- Build for production -->
<!-- npm run build -->
💡
Vue Composition API is the modern way! Use script setup and ref()/computed() instead of the Options API. It's cleaner and works better with TypeScript.
💡
Consider Inertia.js for deeper integration! Inertia.js lets you build single-page apps using Laravel controllers + Vue/React components without building a separate API. Used by Laracasts and many production apps.
← Previous Lesson Next Lesson →
🧠

Test your knowledge!

Take a quiz to reinforce what you learned.

Take Quiz →