Blog Post

Blog → Post

Laravel 12 & Vue/Inertia: A Beginner’s Guide

August 18, 2025
Code

Welcome to your first journey into Laravel 12! This tutorial will guide you through creating a modern, dynamic web application using Laravel for the backend and Vue.js with Inertia.js for the frontend. This combination gives you the power and productivity of Laravel with the smooth, single-page-app (SPA) feel of Vue, without the complexity of building a separate API.

We’ll build a simple application to display a list of blog posts. Along the way, you’ll learn about:

  • Project Setup with the new Vue Starter Kit
  • MVC Architecture: Models, Views (as Vue components), and Controllers
  • Routing: How to direct traffic in your app
  • Eloquent Relationships: How to connect different pieces of your data
  • The Service Layer: How to organize your code cleanly
  • Passing Data: Getting information from your backend to your frontend
  • Core Laravel 12 Concepts: The bootstrap/app.php file and HandleInertiaRequests middleware.

Chapter 1: Your First Laravel Project

Let’s start by getting a fresh Laravel 12 project up and running.

1. Prerequisites

Make sure you have the following installed on your system:

  • PHP (8.4)
  • Composer
  • The Laravel Installer (composer global require laravel/installer)
  • Node.js and npm

2. Create the Project with the Vue Starter Kit

Open your terminal and run the Laravel installer. It will interactively guide you through the setup process.

laravel new my-blog-app

The installer will ask you a series of questions. Answer them as follows:

  • Would you like to install a starter kit?vue
  • Would you like any additional starter kits?none
  • Would you like to initialize a Git repository?yes (or your preference)

The installer will create the project, install the Vue starter kit (which includes Inertia, Tailwind CSS, and shadcn-vue), and install all the necessary PHP and Node dependencies for you.

Once it’s finished, navigate into your new project directory:

cd my-blog-app

3. Set Up The Database

By default, Laravel is configured to use MySQL. For this simple tutorial, we’ll use a lightweight SQLite database, which is just a single file. You may keep using MySQL if you have installed MySQL server.

Open the .env file in your project’s root directory and change the database configuration to this:

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

(You can comment out or delete the other DB_ variables).

Now, create the database file:

touch database/database.sqlite

4. Run the Servers

You need two terminal windows open for development:

  • Terminal 1 (Vite Server): This compiles your Vue components and CSS.
npm run dev
  • Terminal 2 (Laravel Server): This serves your main application.
php artisan serve

Now, if you visit http://127.0.0.1:8000 in your browser, you should see the default Laravel 12 welcome page, powered by Vue and Inertia!

Chapter 2: The MVC Architecture

MVC stands for Model-View-Controller. It’s a way of organizing your code that separates concerns, making your application easier to manage.

  • Model: Represents your data. It’s the part of your app that talks directly to your database tables.
  • View: The user interface. In our case, these are our Vue components.
  • Controller: The “traffic cop.” It handles incoming requests, gets data from the Model, and tells the View what to display.

Let’s create the MVC structure for our blog posts.

1. The Model & Migration

We need a Post model and a corresponding database table. Artisan can create both for us at once.

Run this command:

php artisan make:model Post -m

This command does two things:

  • Creates a Model file at app/Models/Post.php.
  • Creates a Migration file in database/migrations/. A migration is like version control for your database schema.

Open the new migration file (it will have a timestamp in the name) and define the schema for our posts table inside the up() method:

// database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php
public function up(): void
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('body');
        // Creates `created_at` and `updated_at` columns
        $table->timestamps();
    });
}

Now, run the migration to create the table in your SQLite database:

php artisan migrate

2. The Controller

Next, let’s create a controller to handle requests related to posts.

php artisan make:controller PostController

This creates a file at app/Http/Controllers/PostController.php.

3. The View (Vue Component)

With Inertia, our views are Vue components. Let’s create a folder and file for our posts page:

Create resources/js/pages/posts/Index.vue.

<script setup>
// This is where we will receive data from the controller
</script>

<template>
  <div class="p-8">
    <h1 class="text-3xl font-bold">My Blog Posts</h1>
    <!-- We will loop through posts here -->
  </div>
</template>

Chapter 3: Routing, Controller Logic, and Passing Data

Now let’s wire everything together. We’ll create a route, add logic to our controller to fetch posts, and pass that data to our Vue component.

1. Define the Route

Open routes/web.php and add a route that points to our PostController.

// routes/web.php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/', function () {
    return Inertia::render('Welcome');
});

// Add this new route for our posts
Route::get('/posts', [PostController::class, 'index']);

2. Add Logic to the Controller

Open app/Http/Controllers/PostController.php. We’ll create an index method. This method will fetch all posts from the database and render our Vue component, passing the posts as a “prop” (property).

Note: seeding the database is usually done via seeder php artisan make:seeder PostsSeeder but we’ll stick to doing it the controller for the sake of the tutorial.

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

use App\Models\Post; // Import the Post model
use Illuminate\Http\Request;
use Inertia\Inertia; // Import Inertia

class PostController extends Controller
{
    public function index()
    {
        // For now, let's create some dummy data.
        // We'll replace this with real data soon.
        Post::firstOrCreate(['title' => 'My First Post', 'body' => 'This is the body of my very first post!']);
        Post::firstOrCreate(['title' => 'Laravel and Vue', 'body' => 'They work so well together with Inertia.js.']);

        // Fetch all posts and pass them to the view
        $posts = Post::latest()->get();

        return Inertia::render('Posts/Index', [
            'posts' => $posts,
        ]);
    }
}

Key Concept: Inertia::render() This is the magic of Inertia. It renders a Vue component from your resources/js/pages directory and passes it an array of data as props.

3. Display the Data in Vue

Now, let’s update resources/js/pages/posts/Index.vue to receive and display the posts.

<script setup>
// Define the props that this component expects to receive.
// The names must match the keys from the controller.
defineProps({
  posts: Array,
});
</script>

<template>
  <div class="max-w-4xl p-8 mx-auto">
    <h1 class="text-3xl font-bold text-center mb-8">My Blog Posts</h1>
    
    <div class="space-y-6">
      <!-- Loop through each post in the `posts` prop -->
      <div v-for="post in posts" :key="post.id" class="p-6 bg-white border rounded-lg shadow-sm">
        <h2 class="text-2xl font-semibold text-gray-800">{{ post.title }}</h2>
        <p class="mt-2 text-gray-600">{{ post.body }}</p>
        <div class="mt-4 text-sm text-gray-500">
          Posted on: {{ new Date(post.created_at).toLocaleDateString() }}
        </div>
      </div>
    </div>
  </div>
</template>

Now, navigate to http://127.0.0.1:8000/posts in your browser. You should see your posts displayed!

Chapter 4: Eloquent Relationships

What if we want to show which user wrote each post? This requires a relationship between Users and Posts. Laravel’s database toolkit, Eloquent, makes this incredibly easy.

A User can have many Posts. A Post belongs to one User.

1. Update the Posts Migration

First, we need to add a user_id column to our posts table. Go back to your create_posts_table migration and add this line:

// database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade'); // Add this
$table->string('title');
$table->text('body');
$table->timestamps();

Since we already ran our migrations, we need to refresh the database. This will delete all your data.

php artisan migrate:fresh

2. Define the Relationships in the Models

Now, we tell the models about their relationship.

// app/Models/User.php
// Add this method to the User model
public function posts()
{
    return $this->hasMany(Post::class);
}

Now open your Post model and add the following relationship

// app/Models/Post.php
// Add this method to the Post model
public function user()
{
return $this->belongsTo(User::class);
}

3. Update the Controller to Use the Relationship

Let’s modify PostController to create a user and associate posts with them. We’ll also use eager loading (with('user')) to efficiently fetch the user data along with the posts, preventing a common performance issue called the “N+1 problem”.

// app/Http/Controllers/PostController.php
use App\Models\Post;
use App\Models\User; // Import User model
use Illuminate\Support\Facades\Hash; // To hash password
use Inertia\Inertia;

class PostController extends Controller
{
    public function index()
    {
        // Find or create a user
        $user = User::firstOrCreate(
            ['email' => 'test@example.com'],
            ['name' => 'Test User', 'password' => Hash::make('password')]
        );

        // Create posts for this user
        $user->posts()->firstOrCreate(['title' => 'My First Post', 'body' => 'This is the body...']);
        $user->posts()->firstOrCreate(['title' => 'Laravel and Vue', 'body' => 'They work so well...']);

        // Eager load the 'user' relationship when fetching posts
        $posts = Post::with('user')->latest()->get();

        return Inertia::render('Posts/Index', [
            'posts' => $posts,
        ]);
    }
}

4. Display the Author in Vue

Finally, update the Index.vue component to show the author’s name. Because we eager-loaded the relationship, post.user is now available.

<!-- ... inside the v-for loop ... -->
<div class="mt-4 flex items-center justify-between">
  <span class="text-sm text-gray-500">
    Author: {{ post.user.name }}
  </span>
  <span class="text-sm text-gray-500">
    Posted on: {{ new Date(post.created_at).toLocaleDateString() }}
  </span>
</div>

Refresh http://127.0.0.1:8000/posts, and you’ll see “Author: Test User” on each post!

Chapter 5: The Service Layer

As your application grows, your controllers can get bloated with logic. A “Service Layer” is a design pattern where you extract business logic into separate classes called Services. This keeps your controllers thin and your logic reusable.

1. Create a Service

Create a new directory: app/Services. Inside it, create a file: app/Services/PostService.php.

// app/Services/PostService.php
namespace App\Services;

use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class PostService
{
    /**
     * Seed initial data and get all posts with their authors.
     *
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getPostsForIndex()
    {
        // Find or create a user
        $user = User::firstOrCreate(
            ['email' => 'test@example.com'],
            ['name' => 'Test User', 'password' => Hash::make('password')]
        );

        // Create posts for this user
        $user->posts()->firstOrCreate(['title' => 'My First Post', 'body' => 'This is the body...']);
        $user->posts()->firstOrCreate(['title' => 'Laravel and Vue', 'body' => 'They work so well...']);

        // Eager load the 'user' relationship and return the posts
        return Post::with('user')->latest()->get();
    }
}

2. Use the Service in the Controller

Now, we can simplify our PostController by using this service. We can use dependency injection to have Laravel automatically provide an instance of our service to the controller’s method.

// app/Http/Controllers/PostController.php
use App\Services\PostService; // Import the service
use Inertia\Inertia;

// Remove the other `use` statements we no longer need here

class PostController extends Controller
{
    public function index(PostService $postService) // Inject the service
    {
        $posts = $postService->getPostsForIndex();

        return Inertia::render('Posts/Index', [
            'posts' => $posts,
        ]);
    }
}

Our controller is now much cleaner! Its only job is to take a request and return a response, while the service handles the business logic.

Chapter 6: Middleware and bootstrap/app.php

Middleware are like layers of an onion. Every request to your application passes through them. They can perform tasks like checking if a user is authenticated or adding data to a response.

1. The bootstrap/app.php File

In Laravel 12, middleware are registered in the bootstrap/app.php file. This file replaces the old Kernel.php. Open it and look for the withMiddleware method. This is where you can add your own custom middleware.

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \App\Http\Middleware\HandleInertiaRequests::class, // This is a key one!
        \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
    ]);

    //
})

2. HandleInertiaRequests.php

The most important middleware for us is app/Http/Middleware/HandleInertiaRequests.php. This middleware, added by the starter kit, does a few critical things:

  • It identifies the root view (resources/views/app.blade.php) that will host your Vue application.
  • It allows you to share data that should be available on every single page (every Vue component). This is perfect for things like the logged-in user’s name, notifications, or flash messages.

Let’s add a shared property. Open app/Http/Middleware/HandleInertiaRequests.php and modify the share method:

// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'auth' => [
            'user' => $request->user(),
        ],
        // Add a custom property available on every page
        'appName' => config('app.name'),
    ];
}

Now, the application name is available as a prop called appName in all your Vue components, without you having to pass it from each controller method. You could access it in any Vue component like this:

<script setup>
defineProps({
  appName: String,
});
</script>

<template>
  <footer>
    <p>© 2024 {{ appName }}</p>
  </footer>
</template>

Conclusion

Congratulations! You’ve successfully built a simple but modern web application with Laravel 12 and the Vue/Inertia stack.

You’ve learned how to:

  • Set up a new project with the Vue Starter Kit.
  • Structure your code using the MVC pattern.
  • Create Models with Migrations and define Eloquent Relationships.
  • Write clean Controllers that use Services for business logic.
  • Render Vue components using Inertia and pass data to them as props.
  • Understand the new bootstrap/app.php file and the role of the HandleInertiaRequests middleware.

From here, you can explore creating forms, handling user authentication, and leveraging the beautiful pre-built UI components provided by shadcn-vue. Welcome to the Laravel ecosystem!