Integrating Laravel with Vue 3 for a Modern Frontend

Integrating Laravel with Vue 3 for a Modern Frontend

Laravel 12 integrates seamlessly with Vue 3 using Vite. This stack gives you Laravel’s expressive backend and Vue’s reactive UI, ideal for SPAs, dashboards, and highly interactive pages. In this guide, you’ll install Vue 3 via Vite, understand the folder structure, import and mount components correctly, call them from Blade, fetch data from controllers, react to broadcast events, and build a practical login form UI with validation.

Install Vue 3 in a Laravel 12 Project (Vite Setup)

Laravel 12 uses Vite by default. Add Vue using the official plugin and enable it in your Vite config.

npm install vue @vitejs/plugin-vueCode language: Bash (bash)

This installs Vue 3 and the official Vite Vue plugin.

// 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(),
    ],
});Code language: JavaScript (javascript)

This configuration tells Vite to compile resources/js/app.js, refresh on Blade changes, and process Vue single-file components.

Understanding Vue Folder Structure in Laravel

Your Vue code lives under resources/js. Here’s the default layout after enabling Vue:

resources/
 └── js/
      ├── app.js          // Vue entry, mounts the app and registers components
      ├── bootstrap.js    // Axios, CSRF, Echo helpers (optional but useful)
      └── components/
           └── ExampleComponent.vue
Code language: plaintext (plaintext)

app.js is your entry file. You can mount a single root Vue component, register many components for use inside Blade, or auto-register all components in the folder for convenience.

Working with app.js: Three Practical Patterns

1) Mount a Single Root Component (SPA-style)

// resources/js/app.js
import './bootstrap';
import { createApp } from 'vue';
import ExampleComponent from './components/ExampleComponent.vue';

createApp(ExampleComponent).mount('#app');Code language: JavaScript (javascript)

Use this when Vue controls the entire page/app (typical SPA approach).

2) Register Multiple Components (Blade + Vue hybrid)

// resources/js/app.js
import './bootstrap';
import { createApp } from 'vue';
import ExampleComponent from './components/ExampleComponent.vue';
import PostList from './components/PostList.vue';

const app = createApp({});
app.component('example-component', ExampleComponent);
app.component('post-list', PostList);
app.mount('#app');Code language: JavaScript (javascript)

This lets you drop Vue widgets into Blade without making the whole page a SPA.

3) Auto-register All Components (scale easily)

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

const app = createApp({});

// Auto-register ./components/*.vue as <ComponentName />
Object.entries(import.meta.glob('./components/*.vue', { eager: true })).forEach(([path, def]) => {
    const name = path.split('/').pop().replace('.vue', '');
    app.component(name, def.default);
});

app.mount('#app');Code language: JavaScript (javascript)

Now any .vue you add under components/ becomes available by its filename (e.g., PostList.vue<PostList></PostList>).

Create and Call a Vue Component from Blade

// resources/js/components/ExampleComponent.vue
<template>
  <div class="p-4">
    <h2>Hello from Vue 3!</h2>
    <button @click="count++">Clicked {{ count }} times</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>Code language: JavaScript (javascript)

A simple counter using the Composition API. Next, call it from a Blade view and ensure Vite loads app.js.

// resources/views/welcome.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Laravel + Vue 3</title>
  @vite('resources/js/app.js')
</head>
<body>
  <div id="app">
    <example-component></example-component>
  </div>
</body>
</html>Code language: PHP (php)

With pattern #2 or #3 in app.js, the component renders where you place <example-component> inside the #app container.

Fetch Data from a Controller and Render in Vue

// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;

use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        return response()->json(Post::latest()->get());
    }
}Code language: PHP (php)

A simple endpoint that returns posts as JSON for Vue to consume.

// resources/js/components/PostList.vue
<template>
  <div>
    <h2 class="mb-2">Posts</h2>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
const posts = ref([]);

onMounted(async () => {
  const res = await fetch('/posts');
  posts.value = await res.json();
});
</script>Code language: JavaScript (javascript)

The component fetches data on mount and renders a reactive list. Drop <post-list> into Blade to display it.

Realtime UX with Events (Broadcast + Vue)

Broadcast Laravel events and listen in Vue to update the UI instantly (e.g., when a new post is created).

// app/Events/NewPostCreated.php
namespace App\Events;

use App\Models\Post;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class NewPostCreated implements ShouldBroadcast
{
    public $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    public function broadcastOn()
    {
        return new Channel('posts');
    }
}Code language: PHP (php)

This event publishes to the public posts channel. Configure Laravel Echo (Pusher or Ably) in bootstrap.js to subscribe from Vue.

// resources/js/components/PostList.vue (add listener)
<script setup>
import { ref, onMounted } from 'vue';

const posts = ref([]);

onMounted(async () => {
  const res = await fetch('/posts');
  posts.value = await res.json();

  if (window.Echo) {
    window.Echo.channel('posts')
      .listen('NewPostCreated', (e) => {
        posts.value.unshift(e.post);
      });
  }
});
</script>Code language: JavaScript (javascript)

When NewPostCreated is fired on the server, the UI prepends the new post instantly for users connected to the channel.

Build a Vue Login Form UI (Controller + Validation)

Let’s create a practical login UI in Vue that talks to a Laravel controller, validates input, and shows server error messages cleanly.

// resources/js/components/LoginForm.vue
<template>
  <form @submit.prevent="submit" class="max-w-sm">
    <h2 class="mb-3">Login</h2>

    <div class="mb-2">
      <label for="email">Email</label>
      <input id="email" v-model="form.email" type="email" required />
      <p v-if="errors.email" class="text-danger">{{ errors.email }}</p>
    </div>

    <div class="mb-3">
      <label for="password">Password</label>
      <input id="password" v-model="form.password" type="password" required />
      <p v-if="errors.password" class="text-danger">{{ errors.password }}</p>
    </div>

    <button type="submit">Sign in</button>
    <p v-if="errors.global" class="text-danger mt-2">{{ errors.global }}</p>
  </form>
</template>

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

const form = reactive({ email: '', password: '' });
const errors = reactive({ email: '', password: '', global: '' });

async function submit() {
  errors.email = errors.password = errors.global = '';

  const res = await fetch('/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify(form)
  });

  if (res.ok) {
    window.location.href = '/dashboard';
    return;
  }

  if (res.status === 422) {
    const data = await res.json();
    errors.email = data.errors?.email?.[0] || '';
    errors.password = data.errors?.password?.[0] || '';
  } else {
    errors.global = 'Invalid credentials or server error.';
  }
}
</script>Code language: JavaScript (javascript)

The component posts JSON to /login with a CSRF token, handles validation (422) errors, and redirects on success.

// resources/views/auth/login.blade.php
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <title>Login</title>
  @vite('resources/js/app.js')
</head>
<body>
  <div id="app">
    <LoginForm></LoginForm>
  </div>
</body>
</html>Code language: PHP (php)

The Blade view provides the CSRF token meta tag and mounts the <LoginForm> component inside #app.

// app/Http/Controllers/Auth/LoginController.php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    public function __invoke(Request $request)
    {
        $validated = $request->validate([
            'email' => 'required|email',
            'password' => 'required|string|min:6',
        ]);

        if (Auth::attempt($validated, true)) {
            $request->session()->regenerate();
            return response()->json(['ok' => true]);
        }

        throw ValidationException::withMessages([
            'email' => ['These credentials do not match our records.'],
        ]);
    }
}Code language: PHP (php)

The single-action controller validates the request, attempts login, regenerates the session, and returns JSON. On failure it throws a 422 with field errors that Vue shows inline.

// routes/web.php
use App\Http\Controllers\Auth\LoginController;

Route::view('/login', 'auth.login')->name('login');
Route::post('/login', LoginController::class);
Route::get('/dashboard', fn () => view('dashboard'))->middleware('auth');Code language: PHP (php)

These routes render the login page, handle the POST login, and protect the dashboard behind auth middleware.

Related deep dives: Mastering Validation Rules in Laravel 12, How to Add Authentication in Laravel 12 (Without Fortify).

Wrapping Up

You integrated Vue 3 into Laravel with Vite, learned the folder structure, registered and mounted components, called them from Blade, fetched data from controllers, reacted to broadcast events, and built a production-ready login form UI with validation. This Laravel + Vue approach pairs a clean backend with a delightful, reactive frontend—perfect for modern apps.

What’s Next

Level up your stack with these related guides:

0 Comments

Leave a Comment

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

Add Comment *

Name *

Email *

Keep Reading...

Laravel Jetstream: A Powerful Starter Kit for Modern Applications
Laravel Jetstream: A Powerful Starter Kit for Modern Applications

Laravel Jetstream is the official Laravel starter kit designed for developers who want more than just basic authentication. It provides a robust…

Laravel Breeze: A Simple and Elegant Starter Kit
Laravel Breeze: A Simple and Elegant Starter Kit

Laravel Breeze is the official starter kit designed for developers who want a simple, minimal, and elegant way to set up authentication…

Laravel 12 Auth Middleware: History, Functionality, and Practical Usage
Laravel 12 Auth Middleware: History, Functionality, and Practical Usage

Auth middleware is Laravel’s gatekeeper for protected routes. In Laravel 12, the auth middleware is an alias to Illuminate\Auth\Middleware\Authenticate and it’s configured…