Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 AJAX CRUD with the Fetch API: Create, Edit and Delete Without Reloading

๐Ÿ› ๏ธ Laravel Tutorials5 min read

Laravel 13 AJAX CRUD with the Fetch API: Create, Edit and Delete Without Reloading

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
Laravel 13 AJAX CRUD with the Fetch API: Create, Edit and Delete Without Reloading

An AJAX CRUD lets users create, edit and delete records without a page reload. In Laravel 13 you build it with a controller that returns JSON, the browser's Fetch API for requests, and a small script that updates the page. No jQuery is needed. This tutorial builds a task list with create, update and delete, including validation errors and the CSRF token.

Targets: Laravel 13 (PHP 8.3 to 8.5), Blade and plain JavaScript.

Step 1: Create the model and migration

Code
php artisan make:model Task -m
PHP
Schema::create('tasks', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->boolean('done')->default(false);
    $table->timestamps();
});
JavaScript
// app/Models/Task.php
protected $fillable = ['title', 'done'];

protected function casts(): array
{
    return ['done' => 'boolean'];
}

Run php artisan migrate.

Step 2: Write a JSON controller

Code
php artisan make:controller TaskController
JavaScript
<?php

namespace App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class TaskController extends Controller
{
    public function index(): View
    {
        return view('tasks', ['tasks' => Task::latest()->get()]);
    }

    public function store(Request $request): JsonResponse
    {
        $data = $request->validate(['title' => ['required', 'string', 'max:255']]);

        return response()->json(Task::create($data), 201);
    }

    public function update(Request $request, Task $task): JsonResponse
    {
        $data = $request->validate([
            'title' => ['sometimes', 'required', 'string', 'max:255'],
            'done' => ['sometimes', 'boolean'],
        ]);

        $task->update($data);

        return response()->json($task);
    }

    public function destroy(Task $task): JsonResponse
    {
        $task->delete();

        return response()->json(null, 204);
    }
}

When a request sends the header Accept: application/json, Laravel returns validation errors as a 422 JSON response instead of a redirect. The script below sends that header.

Step 3: Add the routes

Ruby
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;

Route::get('/tasks', [TaskController::class, 'index']);
Route::post('/tasks', [TaskController::class, 'store']);
Route::put('/tasks/{task}', [TaskController::class, 'update']);
Route::delete('/tasks/{task}', [TaskController::class, 'destroy']);

These are web routes, so they use sessions and CSRF protection. Add ->middleware('auth') if tasks belong to users, and then scope the queries to the current user.

Step 4: Build the Blade page

HTML / XML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Tasks</title>
</head>
<body>
    <form id="task-form">
        <input type="text" id="title" placeholder="New task">
        <button type="submit">Add</button>
        <p id="error" style="color: red;"></p>
    </form>

    <ul id="task-list">
        @foreach ($tasks as $task)
            <li data-id="{{ $task->id }}">
                <span>{{ $task->title }}</span>
                <button class="delete">Delete</button>
            </li>
        @endforeach
    </ul>

    <script src="/js/tasks.js"></script>
</body>
</html>

The CSRF token sits in a meta tag so JavaScript can read it. Put the script file at public/js/tasks.js, or move the code into resources/js and load it with Vite.

Step 5: Write the JavaScript

JavaScript
const token = document.querySelector('meta[name="csrf-token"]').content;

async function api(url, method, body) {
    const response = await fetch(url, {
        method,
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': token,
        },
        body: body ? JSON.stringify(body) : undefined,
    });

    if (response.status === 204) return null;

    const data = await response.json();

    if (!response.ok) throw data;

    return data;
}

const form = document.getElementById('task-form');
const input = document.getElementById('title');
const list = document.getElementById('task-list');
const error = document.getElementById('error');

form.addEventListener('submit', async (event) => {
    event.preventDefault();
    error.textContent = '';

    try {
        const task = await api('/tasks', 'POST', { title: input.value });

        const li = document.createElement('li');
        li.dataset.id = task.id;
        li.innerHTML = '<span></span> <button class="delete">Delete</button>';
        li.querySelector('span').textContent = task.title;
        list.prepend(li);
        input.value = '';
    } catch (e) {
        error.textContent = e.errors?.title?.[0] ?? 'Something went wrong.';
    }
});

list.addEventListener('click', async (event) => {
    if (!event.target.classList.contains('delete')) return;

    const li = event.target.closest('li');

    await api('/tasks/' + li.dataset.id, 'DELETE');
    li.remove();
});

The helper sends JSON and the CSRF header on every call, and throws the error body for non-2xx responses, so validation messages reach the form. The title is inserted with textContent, never innerHTML, so a title containing HTML cannot inject markup. Event delegation on the list means delete buttons added later also work.

How do I edit a task?

Add an Edit button that swaps the span for an input, then call api('/tasks/' + id, 'PUT', { title: newTitle }) and update the span with the returned title. The update method above already supports it.

How do I check that it works?

  1. Open /tasks, add a task and confirm it appears without a reload.
  2. Submit an empty title and confirm the validation message shows.
  3. Delete a task, then refresh to confirm it is gone from the database.

Common mistakes

  • 419 error: the CSRF header is missing. Send X-CSRF-TOKEN from the meta tag.
  • Redirects instead of JSON errors: add the Accept: application/json header.
  • Inserting server text with innerHTML: use textContent.
  • Forgetting authorization: check that the task belongs to the user in the controller.
  • Parsing a 204 response as JSON: an empty response has no body, so skip json().

FAQ

Why Fetch instead of jQuery or Axios?

Fetch is built into every modern browser, so you do not need extra libraries. Axios is also a good choice if you like its interceptors.

Should the routes be in api.php?

For a same-site Blade page, web routes with CSRF are the simplest. Use api.php with tokens for separate frontends or mobile apps.

Can Livewire do this with less code?

Yes. Livewire removes the manual JavaScript, at the cost of learning its component model.

References

Article Index & Topic Tags
LaravelJavascriptCrudAjax

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.