Laravel with Vue.js
LaravelLaravel 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 does | Vue.js does |
|---|---|
| API routes & controllers | Dynamic UI without page reload |
| Database queries | Reactive data binding |
| Authentication | Component-based UI |
| File uploads | Real-time updates |
| Business logic | Form 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 axiosvite.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.
Test your knowledge!
Take a quiz to reinforce what you learned.