There are quite a lot of articles/videos on Laravel + Vue CRUD, but not enough is published on the newest Vue.js 3 version, using the new Composition API. So, with this step-by-step detailed article, let’s fill in that gap, building a simple Company management form.

Install Laravel and Laravel Breeze

We start from the very beginning, by installing a fresh Laravel project, and a Laravel Breeze starter kit:

laravel new project
cd project
// editing .env file here
composer install
php artisan migrate
composer require laravel/breeze
php artisan breeze:install
npm install && npm run dev

By this point, we should have a default Laravel Breeze with Tailwind CSS design, and Login/Register functionality:

reating Model and API CRUD

We will manage one table called Companies, with four text fields: nameemailaddresswebsite.

So, we create the model, and automatically create migrations with -m:

php artisan make:model Company -m

This is the DB structure: database/migrations/xxxxx_create_companies_table.php:

class CreateCompaniesTable extends Migration
{
    public function up()
    {
        Schema::create('companies', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email');
            $table->string('address')->nullable();
            $table->string('website')->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('companies');
    }
}

Then, of course, we run the migration:

php artisan migrate

In the app/Company.php model, we make all fields fillable:

class Company extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'email', 'address', 'website'];
}

Next, we create a Controller, with a few flags to generate exactly what we need:

php artisan make:controller Api/CompanyController --resource --api --model=Company

Personally, I like to use API Resources to transform the data. Although in this project, we won’t make any transformations, I still have a habit of generating them:

php artisan make:resource CompanyResource

And, inside of app/Http/Resources/CompanyResource.php, there’s this default code:

class CompanyResource extends JsonResource
{
    public function toArray($request)
    {
        return parent::toArray($request);
    }
}

Next, for validation, we generate a FormRequest class:

php artisan make:request CompanyRequest

In this case, I will re-use the same validation rules for both store/update functions, so this is the content of app/Http/Requests/CompanyRequest.php:

class CompanyRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'name' => ['required', 'string'],
            'email' => ['required', 'email'],
            'address' => ['nullable', 'string'],
            'website' => ['nullable', 'url'],
        ];
    }
}

We use those API Resource and Form Request classes inside of our app/Http/Controllers/API/CompanyController.php, which has this code:

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\CompanyRequest;
use App\Http\Resources\CompanyResource;
use App\Models\Company;

class CompanyController extends Controller
{
    public function index()
    {
        return CompanyResource::collection(Company::all());
    }

    public function store(CompanyRequest $request)
    {
        $company = Company::create($request->validated());

        return new CompanyResource($company);
    }

    public function show(Company $company)
    {
        return new CompanyResource($company);
    }

    public function update(CompanyRequest $request, Company $company)
    {
        $company->update($request->validated());

        return new CompanyResource($company);
    }

    public function destroy(Company $company)
    {
        $company->delete();

        return response()->noContent();
    }
}

And, we tie it all together to call that Controller from the routes/api.php:

use App\Http\Controllers\Api\CompanyController;

// ...

Route::apiResource('companies', CompanyController::class);

In this simple project, we won’t use any Middleware, the routes are public.

So, at this point, we have an API ready, and if we enter some company manually in the DB, here’s what we get via Postman:

Installing Vue and “Hello World”

Now, we get to the front-end. We install Vue.js, Vue Router, and Vue Loader:

npm install vue@next vue-router@next vue-loader@next

If you don’t use @next, it will install the older Vue.js 2 version, this is not what we want in this article, we want to use Vue 3 with Composition API.

Next, we need to enable the vue-loader, and in the webpack.mix.js we need to add the vue() part to be compiled.

Before:

mix.js('resources/js/app.js', 'public/js').postCss(...

After:

mix.js('resources/js/app.js', 'public/js').vue().postCss(...

Now, we need to create our first Vue.js component. For now, it will not do anything dynamic, just show the “Hello world”, in resources/js/components/companies/CompaniesIndex.vue:


That’s it in this file, for now, we won’t add any > element, we’ll get to that part in the next section.

Now, let’s create our routes file. We will have three routes: company index, create and edit forms. But, for now, let’s stick with the first one, here’s the Vue.js 3 version syntax of resources/js/routes/index.js:

import { createRouter, createWebHistory } from 'vue-router'

import CompaniesIndex from '../components/companies/CompaniesIndex.vue'

const routes = [
    {
        path: '/dashboard',
        name: 'companies.index',
        component: CompaniesIndex
    }
];

export default createRouter({
    history: createWebHistory(),
    routes
})

Next, we add the id=”app” in the main Blade file of our project resources/views/layouts/app.blade.php:


    

Now, we can create our Vue application inside of that #app, here’s the Vue 3 version syntax for it, in resources/js/app.js:

require('./bootstrap');

require('alpinejs');

import { createApp } from 'vue';
import router from './router'

import CompaniesIndex from './components/companies/CompaniesIndex.vue';

createApp({
    components: {
        CompaniesIndex
    }
}).use(router).mount('#app')

Next, let’s prepare the Laravel view to contain the Vue functionality. We need to add this line to routes/web.php file, which would load the dashboard on any other view coming from Vue Router:

Route::view('/{any}', 'dashboard')
    ->middleware(['auth'])
    ->where('any', '.*');

We change the default Laravel Breeze file resources/views/dashboard.blade.php to include the  tag:


    
       ...
    

    

That’s it, let’s run the main command to compile it all:

npm run dev
// or "npm run watch" to watch for changes in live-mode

So, now, after logging into our Laravel Breeze dashboard, we should see this:

Yay, we enabled Vue.js in our project!

List of Companies: Composition API

In Vue 3, the way to create components was changed, with the introduction of Composition API. It allows you to divide the parts of the component to separate files, to then be re-used. The problem appears in larger projects, but I would advise using it even on smaller projects like this one, to get into the habit of separating the concerns, and make code more readable.

So if you came here with some Vue 2 background, the code below will not look too familiar to you, so please read the official Vue documentation on Why Composition API, and also this article: Vue 3 Composition API vs. Options API.

Notice: you still can use the “old way” of components in Vue 3 as well, but, while preparing for this article, I asked my Twitter audience, and many people with Vue 3 actually do use the Composition API:

To have a CRUD of Companies, we will use a Composition API thing called Composable, which is a separate file that will contain all the methods we need. It’s kind of like a Service in Laravel if you wish.

So, we create resources/js/composables/companies.js, with this code:

import { ref } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'

export default function useCompanies() {
    const company = ref([])
    const companies = ref([])

    const errors = ref('')
    const router = useRouter()

    const getCompanies = async () => {
        let response = await axios.get('/api/companies')
        companies.value = response.data.data
    }

    const getCompany = async (id) => {
        let response = await axios.get(`/api/companies/${id}`)
        company.value = response.data.data
    }

    const storeCompany = async (data) => {
        errors.value = ''
        try {
            await axios.post('/api/companies', data)
            await router.push({ name: 'companies.index' })
        } catch (e) {
            if (e.response.status === 422) {
                for (const key in e.response.data.errors) {
                    errors.value += e.response.data.errors[key][0] + ' ';
                }
            }
        }

    }

    const updateCompany = async (id) => {
        errors.value = ''
        try {
            await axios.patch(`/api/companies/${id}`, company.value)
            await router.push({ name: 'companies.index' })
        } catch (e) {
            if (e.response.status === 422) {
                for (const key in e.response.data.errors) {
                    errors.value += e.response.data.errors[key][0] + ' ';
                }
            }
        }
    }

    return {
        errors,
        company,
        companies,
        getCompany,
        getCompanies,
        storeCompany,
        updateCompany
    }
}

A few things to note here.

The usual standard name of the function of Composable is useSomething(), and it doesn’t necessarily need to be the same as the filename.

Next, we use axios for the API requests, which is by default included in Laravel installation, we just need to import it and can make the requests like axios.get() and axios.post().

Next, what is that ref thing? It is shorter for “reference”, and as it is said in the official Vue 3 documentation, “In Vue 3.0 we can make any variable reactive anywhere with a new ref function”. So, when we define our variables with, for example, const companies = ref([]) syntax, now whenever we call getCompaniescompanies will be mutated and the view will be updated to reflect the change.

The next thing to notice is that we use Vue Router to redirect to the list, by its name companies.index, after a successful store/update method. In case of validation errors with code 422, we parse the error structure and turn it into a String variable of errors.

Finally, we define what we return from the Composable file – all variables and methods.

Now, let’s fill them into our resources/js/components/companies/CompaniesIndex.vue with the actual table of data about the companies. Here’s our updated code, the table in the