Laravel 12 CRUD With Vue JS 3 Tutorial Step-by-Step Guide

In this Laravel guide titled “Laravel 12 CRUD With Vue JS 3 Tutorial Step-by-Step Guide”, you’ll learn how to build a complete CRUD (Create, Read, Update, Delete) application using Laravel 12 and Vue.js 3.

Building a modern web application requires a powerful backend and a reactive frontend. In this tutorial, you’ll learn how to build a complete CRUD (Create, Read, Update, Delete) application using Laravel 12 as the backend API and Vue.js 3 as the frontend.

By the end of this guide, you’ll have a fully functional CRUD system with Laravel 12 and Vue 3 working together smoothly.

What We’ll Build

We’ll create a simple Post Management System where users can:

  • Create a new post
  • View post
  • Edit a post
  • Delete a post
  • List all posts

Tech Stack:

  • Laravel 12 (REST API)
  • Vue JS 3 (Composition API)
  • Axios (HTTP requests)
  • MySQL (Database)
  • SweetAlert2 For flash message

Prerequisites

Ensure the following are installed on your system:

  • PHP 8.2 or higher
  • Composer
  • Node.js (v18+) and npm
  • Basic knowledge of Laravel and Vue.js

Check versions:

php -v
composer -V
node -v
npm -v

Step 1: Install New Laravel Project

Run the command below to create a new Laravel project:

composer create-project laravel/laravel laravel-12-vue-crud
cd laravel-12-vue-crud

Step 2: Configure Database

Open .env file and update database settings:

DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

Step 3: Install Laravel/UI Package

Run the following command using Composer:

composer require laravel/ui

Now, run the following command to generate the basic Vue.js scaffolding in your Laravel project:

php artisan ui vue

Step 4: Install Frontend Dependencies and Compile Assets

After installing the UI package, when I ran npm install, I encountered the following dependency error:

npm ERR! node_modules/vite
npm ERR! dev vite@”^7.0.7″ from the root project

npm ERR! Could not resolve dependency:
npm ERR! peer vite@”^4.0.0 || ^5.0.0″ from @vitejs/plugin-vue@4.6.2
npm ERR! node_modules/@vitejs/plugin-vue
npm ERR! dev @vitejs/plugin-vue@”^4.5.0″ from the root project

This issue occurs because the installed version of Vite (v7) is not compatible with the older version of @vitejs/plugin-vue.

To resolve the problem, update the @vitejs/plugin-vue version in your package.json file:

"@vitejs/plugin-vue": "^6.0.0",

After updating the version, run the following command again:

npm install

This should resolve the dependency conflict and complete the installation successfully. After the installation is complete, compile the frontend assets by running:

npm run dev

Next, we will install the Laravel Sanctum package. In Laravel 12, the api.php route file is not available by default. To generate the API routes file, we need to install using the following command.

php artisan install:api

This command automatically:

  • Installs Laravel Sanctum package
  • Creates the api.php routes file
  • Publishes Sanctum configuration
  • Creates the personal_access_tokens migration table

Run the migrations to create necessary database tables:

php artisan migrate

Next, install SweetAlert2 to enable toast-style notifications for posts.

Open your terminal and run the following command to add SweetAlert2 to your project:

npm install sweetalert2

Create a new file at resources/js/services/alert.js, then add the following code to it:

import Swal from 'sweetalert2'
 
// Toast
export const Toast = Swal.mixin({
    toast: true,
    position: 'top-end',
    showConfirmButton: false,
    timer: 3000,
    timerProgressBar: true,
    background: '#28a745',
    color: '#fff'  
})
 
// Confirmation dialog
export const confirmDialog = (text= "You won't be able to revert this!") => {
    return Swal.fire({
        title: 'Are you sure?',
        text: text,
        icon: 'warning',
        showCancelButton: true,
        confirmButtonText: 'Yes',
        cancelButtonText: 'Cancel',
    })
}

Read Also : Laravel 12 AJAX CRUD Operation: Step-by-Step Guide

Step 5: Create Model, Migration & Controller

We will create a Post model with migration and API controller.

php artisan make:model Post -mcr

This command creates:

  • Model: Post
  • Migration file
  • API Controller: PostController

Update Migration File

Open the generated migration in database/migrations/xxxx_xx_xx_xxxxxx_create_posts_table.php :

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string("title");
            $table->text("content");
            $table->string("image");
            $table->tinyInteger("status")->default(0);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Run migration:

php artisan migrate

Update Post Model

Open app/Models/Post.php and add fillable:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Post extends Model
{
     /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'title',
        'content',
        'image',
        'status',
    ];
}

Step 6: Update PostController for Vue CRUD

Open app/Http/Controllers/PostController.php and add the following:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use File;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $posts = Post::latest()->paginate(10);

        return response()->json(['data'=>$posts],200);

    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        $validated   =  $request->validate([
                'title' => 'required|string|max:255',
                'content' => 'required|string',
                'status' =>'required|string',
                'image'  => 'required|image|mimes:jpeg,png,jpg|max:2048',
            ]);

        
         // Handle image upload
        if ($request->hasFile('image')) {
 
            $image = $request->file('image');
 
            // Generate a unique name
            $imageName = time() . '.' . $image->getClientOriginalExtension(); 
 
            // Define the destination path within the public folder
            $destinationPath = public_path('posts'); // Creates 'public/images'
 
            // Move the uploaded file to the public folder
            $image->move($destinationPath, $imageName);
 
            $validated['image'] = 'posts/'.$imageName;
        }
       
           
        $post = Post::create($validated);
      
        return response()->json(['msg'=>'Post Created Successfully!'],201);
    }

    /**
     * Display the specified resource.
     */
    public function show(Post $post)
    {
        return $post;
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, Post $post)
    {
        $validated   =  $request->validate([
                'title' => 'sometimes|required|string|max:255',
                'content' => 'sometimes|required|string',
                'image'  => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
                'status' =>'sometimes|required|string',
            ]);

        
         // Handle image upload
        if ($request->hasFile('image')) {

            $old_image = public_path($post->image);
            // Delete old image if exists
            if (File::exists($old_image)) {
                File::delete($old_image);
            }
 
            $image = $request->file('image');
 
            // Generate a unique name
            $imageName = time() . '.' . $image->getClientOriginalExtension(); 
 
            // Define the destination path within the public folder
            $destinationPath = public_path('posts'); // Creates 'public/images'
 
            // Move the uploaded file to the public folder
            $image->move($destinationPath, $imageName);
 
            $validated['image'] = 'posts/'.$imageName;
        }
           
        $post->update($validated);
      
        return response()->json(['msg'=>'Post Updated Successfully!'],204);
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Post $post)
    {
        $image = public_path($post->image);
          // Delete image if exists
        if ($post->image && File::exists($image)) {
            File::delete($image);
        }
 
        $post->delete();
        return response()->json(['msg'=>'Post Deleted Successfully!'],200);
    }
}

Explanation

index()

  • Fetches latest posts from database.
  • Uses pagination (10 posts per page).

store(Request $request)

  • Validates request data (title, content, status, image).
  • Uploads image to public/posts folder.
  • Saves image path in database.

show(Post $post)

  • Displays single post data.
  • Uses Laravel Route Model Binding to automatically fetch the post.

update(Request $request, Post $post)

  • Validates updated fields (sometimes means optional).
  • If new image uploaded:
    • Deletes old image from folder.
    • Uploads new image to public/posts.
  • Updates post data using $post->update().

destroy(Post $post)

  • Checks if post image exists.
  • Deletes image file from folder.
  • Deletes the post from database.

Read Also : Laravel 12 with Vue.js Complete Setup Guide for Beginners

Step 7: Create Vue Component

Create file: resources/js/components/Posts.vue

<template>
  <div class="card-body mb-2">
    <button
      class="btn btn-primary btn-sm mb-3"
      data-bs-toggle="modal"
      data-bs-target="#create_post_modal" @click="openCreate"
    >
      <i class="fa-regular fa-plus" ></i> Create Post
    </button>
    <div class="table_data">
      <table class="table table-bordered">
        <thead>
          <tr>
            <th>#</th>
            <th>Title</th>
            <th>Status</th>
            <th>Image</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          <tr v-if="posts.length" v-for="(post,index) in posts" :key="post.id">
            <td>{{serial+index}}</td>
            <td>{{post.title}}</td>
            <td>{{ post.status == 1 ? 'Active' : 'Inactive'}}</td>
            <td>
             <a target="_blank" :href="post.image ? '/' + post.image : '/posts/default.jpg'">
              <img :src="post.image ? '/' + post.image : '/posts/default.jpg'" class="img img-thumbnail" width="80px"/>
             </a>
            </td>
            <td>
              <button
                class="btn btn-primary btn-sm "
               @click="editPost(post)"
              >
                <i class="fa-regular fa-pen-to-square"></i> Edit
              </button>
              |
              <button
                class="btn btn-success btn-sm show"
                @click="showPost(post)">
                <i class="fa-regular fa-eye"></i> Show
              </button>
              |
              <button class="btn btn-danger btn-sm delete" @click="deletePost(post.id)">
                <i class="fa-solid fa-trash"></i> Delete
              </button>
            </td>
          </tr>
          <tr v-else-if="posts.length==0">
          <td colspan="5" class="text-center fs-4"> 
              Post Not Found!
            </td>
          </tr>
          <tr v-else>
          <td colspan="5" class="text-center fs-4"> 
           <i class="fa fa-spinner fa-spin"></i>Processing...
          </td>
          </tr>
        </tbody>
      </table>

      <nav aria-label="Page navigation example">
        <ul class="pagination justify-content-end">
          <li
            v-for="(link,index) in links"
            :key="index"
            :class="{
                    'page-item':link.active && link.url,
                    'page-item active':link.active,
                    'page-item disabled':!link.url,
                    }"
          >
            <a
              class="page-link"
              href="#"
              @click="fetchPosts(link.url)"
              :disabled="!link.url"
              v-html="link.label"
            ></a>
          </li>
        </ul>
      </nav>
    </div>
  </div>
 <forms @post-created="getPosts" :editPost="post" @post-updated="fetchPosts('api/posts?page='+current_page)"     />
 <show :post="post" />  
</template>

<script setup>
import { ref, reactive, onMounted } from "vue"
import axios from 'axios';
import { Toast, confirmDialog } from '@/services/alert';


const posts = ref({})
const links = ref({})
const serial = ref(0)
const errors = ref({})
const successMessage = ref("")
const current_page = ref('')
const post = reactive({
                id:"",
                title:"",
                status:"",
                content:"",
                image:""
            })
 

/* methods */
       async function getPosts()  {
           const res = await axios.get("/api/posts");
           posts.value = res.data.data.data;
           links.value = res.data.data.links;
           serial.value  = res.data.data.from;
           current_page.value = res.data.data.current_page;

        }
        async function fetchPosts(url) {          
           if (!url) return
           const res = await axios.get(url);
           posts.value = res.data.data.data;
           links.value = res.data.data.links;
           serial.value  = res.data.data.from;
           current_page.value = res.data.data.current_page;           

        }

          async function showPost(selectedPost) {
           Object.assign(post, selectedPost)
          $("#show_modal").modal('show');

          }

      

      
        function editPost(selectedPost) {
           Object.keys(post).forEach(key => post[key] = '')
            Object.assign(post, selectedPost)          
            $('#create_post_modal').modal('show');
        }

        function deletePost(id){
          confirmDialog(
            "You will not able to revert this!"
          ).then((result) => {

                if (result.value) {
                     axios.delete('/api/posts/' + id).then((res) => {
                      Toast.fire({
                        icon: 'success',
                        title: res.data.msg,
                      });
                      getPosts();
                     }).catch(() => {
                        Toast.fire({
                            icon: 'error',
                            title: 'Oops...',
                            text: 'Something went wrong!',
                            background:'red'
                        })
                     })
                }
          });
        }
       function openCreate(){
        post.id = null
        post.title = ""
        post.status = ""
        post.content = ""
        post.image = null
        }

     /* -------------------- LIFECYCLE -------------------- */

      onMounted(() => {
              getPosts();

        })

</script>

UI Part

Create Post Button

  • Opens modal #create_post_modal.
  • Calls openCreate() to reset form.

Action Buttons

  • Edit : editPost(post) opens edit modal.
  • Show : showPost(post) opens view modal.
  • Delete : deletePost(post.id) deletes post.

Loading Row

  • Shows Processing spinner while loading.

Pagination

  • links loop creates page buttons.
  • Clicking calls fetchPosts(url).

Child Components

  • <forms> : Create/Edit post form.
  • <show> : View post details.

Methods

getPosts()

  • Calls API /api/posts.
  • Loads posts and pagination.

fetchPosts(url)

Fetches posts for pagination link.

showPost(selectedPost)

  • Copies post data to post.
  • Opens show modal.

editPost(selectedPost)

  • Clears previous data.
  • Copies selected post data.
  • Opens edit modal.

deletePost(id)

  • Shows confirmation dialog.
  • If confirmed:
    • Calls API DELETE /api/posts/{id}.
    • Shows success toast.
    • Reloads posts.

openCreate()

  • Resets post data.
  • Used when creating new post.

Lifecycle Hook

onMounted()

  • Runs when component loads.
  • Calls getPosts() to load posts.

Step 8: Post Create/Edit Component

Create resources/js/components/Forms.vue:

<template>

  <!--Create modal --> 
     <div class="modal fade" id="create_post_modal"  tabindex="-1" aria-labelledby="exampleModalLabel" >
        <div class="modal-dialog">
         <div class="modal-content">
            <div class="modal-header">
                <h1 class="modal-title fs-5" id="exampleModalLabel">{{post.id?"Update Post":"Create Post"}}</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"  @click="closeForm()"></button>
            </div>
            <div class="modal-body">
                <div class="alert alert-success print-success-msg" v-if="successMessage">
                    {{successMessage}}
                </div>
                <div v-if="isFormLoading" class="text-center my-3">
                  <i class="fa fa-spinner fa-spin fa-2x text-success"></i>
                </div>
                <form method="post" enctype="multipart/form-data" id="add_post_form" @submit.prevent="savePost">                   
                  
                <div class="mb-3">
                    <label>Post Title</label>
                    <input type="text" name="title" class="form-control" v-model="post.title" :class=" errors?.title ? 'is-invalid': '' "/>
                         <span v-if="errors?.title" class="invalid-feedback">{{ errors.title[0] }}  </span>
                </div>                
                <div class="mb-3">
                    <label>Status</label>
                      <select class="form-control" name="status" v-model="post.status" :class=" errors?.status ? 'is-invalid': ''">
                        <option value="draft">draft</option>
                        <option value="publish">publish</option>
                      </select>
                      <span v-if="errors?.status" class="invalid-feedback">{{ errors.status[0] }}  </span>
                </div>
                <div class="mb-3">
                    <label>Content</label>
                    <textarea  name="content" class="form-control" v-model="post.content" :class=" errors?.content ? 'is-invalid': ''">
                    </textarea>
                    <span v-if="errors?.content" class="invalid-feedback">{{ errors.content[0] }}  </span>
                </div>
                 <div class="mb-3">
                    <label>Image</label>
                    <input type="file" ref="postImage"  name="image" class="form-control"   :class="errors?.image ? 'is-invalid' : ''"
                @change="handleImage" />
                    
                    <span v-if="errors?.image" class="invalid-feedback">{{ errors.image[0] }}  </span>
                    
                </div>
                <div class="mb-3 text-center">
                    <button class="btn btn-success btn-submit" type="submit"  :disabled="isSubmitting">
                      <span v-if="isSubmitting">
                        <i class="fa fa-spinner fa-spin"></i> Processing...
                      </span>

                      <span v-else>
                        <i v-if="!post.id" class="fa fa-save"></i>
                        {{ post.id ? 'Update' : 'Save' }}
                      </span>
                    </button>
                </div>
             </form>
            </div>
            
         </div>
        </div>
    </div>
     <!-- End -->
</template>

<script setup>
import { ref, watch,reactive } from 'vue'
import axios from 'axios';
import { Toast } from '@/services/alert'

// props from parent
const props = defineProps({
  editPost: Object // null = create, object = update
})


// emit to parent
const emit = defineEmits(['close','post-created','post-updated'])

/* state */
const errors = reactive({})
const successMessage = ref("")
const postImage = ref(null)
// For edit mode loading (body)
const isFormLoading = ref(false)
// For submit button loading   
const isSubmitting = ref(false)   

const post = reactive({
  id: null,
  title: "",
  status: "",
  content: "",
  image: null
})

/* When editPost changes = fill form */
watch(() => props.editPost, (val) => {
  isFormLoading .value = true
  if (val) { 
    Object.assign(post, { ...val})
    setTimeout(() => {
      isFormLoading.value = false
    }, 100)    
  } else {
    resetForm()
  }
},
  { deep: true, immediate: true })

/* methods */

const resetForm = () => {

  post.id = null
  post.title = ""
  post.status = ""
  post.content = ""
  post.image = null
  
  //Object.keys(post).forEach(key => post[key] = '')
  if(postImage.value) postImage.value.value=""
  Object.keys(errors).forEach(key => delete errors[key])
}

const handleImage = (e) => {
  post.image = e.target.files[0]
}
const closeForm = () =>{
   resetForm()
}
const savePost = async () => {
  try {
    isSubmitting.value = true
    const formData = new FormData()

    formData.append('title', post.title)
    formData.append('status', post.status)
    formData.append('content', post.content)
    if (post.image instanceof File) {
    formData.append('image', post.image)
    }

     let res

    if (post.id) {
      // UPDATE
      formData.append('_method', 'PUT')
      res = await axios.post(`/api/posts/${post.id}`, formData)
      emit('post-updated') 
      $('#create_post_modal').modal('hide');    
      resetForm()
      
    } else {
      // CREATE
      res = await axios.post('/api/posts', formData)
      emit('post-created')
      resetForm()
    }

    
   let msg = post.id ? "Post Updated Successfully!"
        : "Post Created Successfully!"
    Toast.fire({
      icon: 'success',
      title: msg,
    })

    
    
    

  } catch (error) {

    if (error.response?.status === 422) {
      Object.keys(errors).forEach(key => delete errors[key])
      Object.assign(errors, error.response.data.errors)
    } else {
      console.error(error)
    }

  }finally {
    isSubmitting.value = false  // STOP LOADING
  }
}

</script>

UI Part

  • Bootstrap modal used for Create / Update Post.
  • Shows spinner when isFormLoading = true.
  • Uses @submit.prevent=”savePost” to prevent page reload and call savePost().
  • @change=”handleImage” saves selected file.
  • Form Submit Button disabled when submitting (isSubmitting).
  • Shows: Processing spinner while saving. Save / Update text depending on mode.

Props (from Parent)

  • editPost
    • Receives post data from parent component.
    • Used to fill form when editing.

Emits (to Parent)

  • post-created : refresh list after create.
  • post-updated : refresh list after update.
  • close : close form.

watch(props.editPost)

  • Runs when edit post changes.
  • Fills form with selected post data.
  • Or resets form if creating new.

Methods

resetForm()

  • Clears form fields.
  • Clears errors.
  • Clears file input.

handleImage(e)

  • Gets selected image file.
  • Saves it in post.image.

closeForm()

  • Calls resetForm() when modal closes.

savePost()

Handles Create + Update API request.

  • Start loading (isSubmitting).
  • Create FormData.
  • Append title, status, content, image.
  • If post.id exists : Update API
  • Else : Create API
  • Emit event to parent.
  • Show success toast.
  • Handle validation errors (422).

Step 9: Show Post Component

Create resources/js/components/Show.vue:

  <template>
  <!--Create modal --> 
     <div class="modal fade" id="show_modal" tabindex="-1" aria-labelledby="exampleModalLabel">
        <div class="modal-dialog">
            <div class="modal-content">
            <div class="modal-header">
                <h1 class="modal-title fs-5" id="exampleModalLabel">
                  <i class="fa-regular fa-eye"></i>  Show Post
                </h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                 <p><strong>Title:</strong> <span class="title">{{post.title}}</span></p>
                 <p><strong>Status:</strong> <span class="status">{{ post.status}}</span></p>
                 <p v-if="post.image"><strong>Image:</strong><img :src=" '/' + post.image" class="img img-thumbnail" width="200"/></p>
                <p><strong>Content:</strong> <span class="content">{{post.content}}</span></p>
            </div>
            
            </div>
        </div>
    </div>
     <!-- End -->
</template>

<script setup>
defineProps({
  post: Object
})
</script>

Explanation

  • Bootstrap modal used to display post details.
  • Receives post data from parent component.
  • Used to display post details in modal.

Step 10: Update Vue App

Update resources/js/app.js:

import { createApp } from 'vue';

const app = createApp({});

import Posts from './components/Posts.vue'; 
import Forms from './components/Forms.vue'; 
import Show from './components/Show.vue'; 


app.component('post-component', Posts);
app.component('forms', Forms);
app.component('show', Show);


app.mount('#app');

Step 11: Update Welcome Blade

Open resources/views/welcome.blade.php and update with the following code.

<!DOCTYPE html>
<html>
<head>
    <title>Laravel 12 CRUD Vue JS 3 Tutorial – Step-by-Step Guide</title>
     <!-- Styles / Scripts -->
        @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
            @vite(['resources/css/app.css', 'resources/js/app.js'])
        @endif
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
    <meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
    <div class="container mt-5">
        <div class="card">
            <h5 class="card-header bg-primary text-white">Laravel 12 CRUD Vue JS 3 Tutorial – Step-by-Step Guide - ItStuffSolutiotions</h5>
          
            <div id="app">
               
                <post-component></post-component>
            </div>
        </div>
    </div>
  
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
     <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

   
</body>

</html>

Step 12: Run the Application

We have completed all the steps successfully Next, Start Laravel server:

php artisan serve

visit the url http://127.0.0.1:8000

You now have a fully working Laravel 12 CRUD application with Vue 3.

Laravel 12 CRUD With Vue JS 3 Tutorial Step-by-Step Guide
Laravel vue js Form Preview

Conclusion

In this Laravel tutorial, we built a complete Laravel 12 CRUD Vue JS 3 Application step by step from scratch. Laravel 12 provides a powerful API backend, while Vue 3 delivers a reactive and smooth frontend experience.

Combining Laravel 12 backend with Vue 3 frontend gives you:

  • Fast development
  • Clean API structure
  • Scalable application architecture
  • Modern SPA experience

Frequently Asked Questions (FAQs)

Why use Vue.js 3 with Laravel 12?

Vue.js 3 works very well with Laravel because it provides a reactive frontend framework that integrates easily with Laravel APIs. Using Vue 3 allows developers to build dynamic single-page applications (SPAs) while Laravel handles the backend logic and database operations.

What is the Composition API in Vue 3?

The Composition API is a modern way of writing Vue components introduced in Vue 3. It allows developers to organize logic using functions such as ref, reactive, and onMounted, making the code more reusable and easier to maintain compared to the Options API.

Do I need to install Vue Router for this CRUD project?

No, Vue Router is not required for this basic CRUD tutorial. However, if you want to create a more advanced single-page application with multiple pages or routes, installing Vue Router would be beneficial.

Why is Axios used in this tutorial?

Axios is a popular JavaScript library used for making HTTP requests from the frontend to the backend API. In this tutorial, Axios is used to send requests to Laravel API endpoints to create, fetch, update, and delete posts.

Can I use this Laravel 12 Vue 3 CRUD setup for a production project?

Yes, but you should enhance it for production by adding features such as:
Authentication (Laravel Sanctum or Passport)
Form validation and error handling
API resource responses
Pagination
Security measures and rate limiting

Can I use Tailwind CSS or Bootstrap with this project?

Yes. You can easily add Tailwind CSS for CRUD application. Laravel works very well with both styling frameworks.

Is Laravel 12 suitable for building APIs?

Yes. Laravel is widely used for building RESTful APIs because it provides powerful tools such as API resources, authentication, middleware, and validation.