Kembali ke Beranda Blog
Web Development 5 min read 16 pembaca

Laravel 13 Masterclass: Build a Real-World Application

A hands-on, multi-chapter engineering tutorial building a full-stack Task Management & Audit System using Laravel 13. Features step-by-step file placement, modern strict typing, and practical use of new framework features.

Dahlan

Dahlan

Software Architect • 24 Agustus 2026

Laravel 13 Masterclass: Build a Real-World Application

Building a Production-Ready Application with Laravel 13: A Complete Hands-On Guide

1. What's New in Laravel 13? Engine Upgrades & Architectural Highlights

Laravel 13 introduces substantial performance improvements and structural refinements designed to reduce boilerplate and elevate developer productivity. Building on the streamlined directory layout where configuration files and middleware stacks are consolidated into bootstrap/app.php, Laravel 13 advances the framework's internal architecture across several core areas.

Key Laravel 13 Framework Innovations

Enhanced Native PHP 8.3/8.4 Support:

Fully leverages typed class constants, asymmetric property visibility, and deep performance tuning within the core service container.

Streamlined Concurrency & Deferred Tasks:

Expands the native defer() helper primitives to defer execution of non-blocking background tasks without requiring complex queue worker infrastructure for lightweight operations.

Refined Middleware & Exception Handling:

Routes, health checks, rate limiters, and custom exception renderers are defined expressively inside bootstrap/app.php using fluent builder APIs.

Automated Route Pre-Compilation:

Boosts route resolution performance by compiling named routes directly into native array trees during container startup.

Advanced Eloquent Attribute Hooks:

Streamlines casting, accessors, and mutators into unified Attribute methods with superior static analysis support.

Chapter 2. Setting Up the Development Environment and Creating the Project

To build our project, TaskPulse, ensure your workstation has PHP 8.3 or higher, Composer 2.7+, and SQLite/MySQL active.

Open your terminal and run the following command to create a fresh Laravel 13 project:

Bash

composer create-project laravel/laravel:^13.0 taskpulse
cd taskpulse

Next, configure your environment file to use SQLite for rapid local development. Modify the .env file in the project root:

File: .env

Code snippet

APP_NAME=TaskPulse
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000

DB_CONNECTION=sqlite
# DB_HOST, DB_PORT, DB_DATABASE can be commented out for SQLite

Generate the encryption application key and create the SQLite database file:

Bash

php artisan key:generate
touch database/database.sqlite

Chapter 3. Database Architecture: Designing Migrations for Tasks & Audits

Our application requires two primary database tables: tasks and audit_logs. We will construct these using database migrations.

Run the artisan command to generate the migration files:

Bash

php artisan make:migration create_tasks_table
php artisan make:migration create_audit_logs_table

Locate the generated migration file for tasks in database/migrations/ and replace its contents:

File: database/migrations/2026_01_01_000001_create_tasks_table.php

PHP

<?php

declare(strict_types=1);

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->string('status')->default('pending'); // pending, in_progress, completed
            $table->string('priority')->default('medium'); // low, medium, high
            $table->timestamp('due_at')->nullable();
            $table->timestamps();
        });
    }

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

Next, edit the migration file for audit logs:

File: database/migrations/2026_01_01_000002_create_audit_logs_table.php

PHP

<?php

declare(strict_types=1);

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('audit_logs', function (Blueprint $table) {
            $table->id();
            $table->foreignId('task_id')->constrained()->cascadeOnDelete();
            $table->string('action'); // created, status_updated, deleted
            $table->text('details');
            $table->timestamps();
        });
    }

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

Run the migration command to construct the database schema:

Bash

php artisan migrate

Chapter 4. Eloquent Models: Using Asymmetric Visibility & Modern Casts

With the database schema established, create the Eloquent models to represent our domain objects.

Bash

php artisan make:model Task
php artisan make:model AuditLog

In Laravel 13, Eloquent models leverage native type declarations and attribute mutators cleanly. Open app/Models/Task.php and update it:

File: app/Models/Task.php

PHP

<?php

declare(strict_types=1);

namespace App\Models;

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

final class Task extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'description',
        'status',
        'priority',
        'due_at',
    ];

    /**
     * Get the casts array for type conversions.
     */
    protected function casts(): array
    {
        return [
            'due_at' => 'datetime',
            'created_at' => 'datetime',
            'updated_at' => 'datetime',
        ];
    }

    /**
     * Get the audit logs associated with the task.
     * 
     * @return HasMany<AuditLog, $this>
     */
    public function auditLogs(): HasMany
    {
        return $this->hasMany(AuditLog::class);
    }
}

Now, update the AuditLog model file:

File: app/Models/AuditLog.php

PHP

<?php

declare(strict_types=1);

namespace App\Models;

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

final class AuditLog extends Model
{
    use HasFactory;

    protected $fillable = [
        'task_id',
        'action',
        'details',
    ];

    /**
     * Get the task that owns the audit log.
     * 
     * @return BelongsTo<Task, $this>
     */
    public function task(): BelongsTo
    {
        return $this->belongsTo(Task::class);
    }
}

5. Input Validation: Building Robust Form Request Classes

To prevent malformed data from reaching our database, we create custom Form Request validation classes.

Bash

php artisan make:request StoreTaskRequest

Update the generated Form Request to enforce strict validation rules:

File: app/Http/Requests/StoreTaskRequest.php

PHP

<?php

declare(strict_types=1);

namespace App\Http/Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

final class StoreTaskRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // Authorize request execution
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, array<int, mixed>>
     */
    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string', 'max:1000'],
            'priority' => ['required', Rule::in(['low', 'medium', 'high'])],
            'due_at' => ['nullable', 'date', 'after:now'],
        ];
    }
}

Chapter 6. Business Logic: Service Classes & Deferring Deferred Actions

Rather than bloating HTTP controllers with database transactions and log dispatching, we extract business logic into dedicated Service classes. Here, we utilize Laravel 13's non-blocking defer() function to record audit logs in the background without slowing down the primary HTTP response cycle.

Create a new PHP service file manually:

File: app/Services/TaskService.php

PHP

<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\AuditLog;
use App\Models\Task;
use Illuminate\Support\Facades\DB;
use function Illuminate\Support\defer;

final class TaskService
{
    /**
     * Create a new task and asynchronously log an audit record using Laravel 13 defer.
     *
     * @param array<string, mixed> $data
     */
    public function createTask(array $data): Task
    {
        $task = DB::transaction(function () use ($data) {
            return Task::create([
                'title' => $data['title'],
                'description' => $data['description'] ?? null,
                'priority' => $data['priority'],
                'due_at' => $data['due_at'] ?? null,
                'status' => 'pending',
            ]);
        });

        // LARAVEL 13 FEATURE: Defer non-critical background audit logging
        // Executes automatically after the HTTP response has been delivered to the client
        defer(function () use ($task) {
            AuditLog::create([
                'task_id' => $task->id,
                'action' => 'created',
                'details' => "Task '{$task->title}' was successfully created.",
            ]);
        });

        return $task;
    }

    /**
     * Update task status and queue deferred audit entry.
     */
    public function updateTaskStatus(Task $task, string $newStatus): Task
    {
        $oldStatus = $task->status;
        $task->update(['status' => $newStatus]);

        defer(function () use ($task, $oldStatus, $newStatus) {
            AuditLog::create([
                'task_id' => $task->id,
                'action' => 'status_updated',
                'details' => "Task status changed from {$oldStatus} to {$newStatus}.",
            ]);
        });

        return $task;
    }
}

Chapter 7. Controllers: Handling Web Requests Cleanly

Now, create a controller to receive incoming HTTP requests, invoke the service layer, and render user interfaces or JSON payloads.

Bash

php artisan make:controller TaskController

Edit the generated controller file:

File: app/Http/Controllers/TaskController.php

PHP

<?php

declare(strict_types=1);

namespace App\Http/Controllers;

use App\Http\Requests\StoreTaskRequest;
use App\Models\Task;
use App\Services\TaskService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

final class TaskController extends Controller
{
    public function __construct(
        private readonly TaskService $taskService
    ) {}

    public function index(): View
    {
        $tasks = Task::with('auditLogs')
            ->latest()
            ->paginate(10);

        return view('tasks.index', compact('tasks'));
    }

    public function store(StoreTaskRequest $request): RedirectResponse
    {
        $this->taskService->createTask($request->validated());

        return redirect()
            ->route('tasks.index')
            ->with('success', 'Task created successfully with deferred audit logging!');
    }

    public function updateStatus(Request $request, Task $task): RedirectResponse
    {
        $validated = $request->validate([
            'status' => ['required', 'in:pending,in_progress,completed'],
        ]);

        $this->taskService->updateTaskStatus($task, $validated['status']);

        return redirect()
            ->route('tasks.index')
            ->with('success', 'Task status updated!');
    }
}

Chapter 8. Routing Architecture: Fluent Routing in Laravel 13

Routing in Laravel 13 is concise and centralized in routes/web.php. Open the web routes file and register our application endpoints.

File: routes/web.php

PHP

<?php

declare(strict_types=1);

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

Route::get('/', function () {
    return redirect()->route('tasks.index');
});

// Grouped RESTful Routes for Task Management
Route::controller(TaskController::class)->group(function () {
    Route::get('/tasks', 'index')->name('tasks.index');
    Route::post('/tasks', 'store')->name('tasks.store');
    Route::patch('/tasks/{task}/status', 'updateStatus')->name('tasks.update-status');
});

Chapter 9. Middleware & Exception Handling in bootstrap/app.php

In Laravel 13, global middleware configurations, custom headers, and exception responses are managed inside bootstrap/app.php rather than spread across multiple kernel files.

Update bootstrap/app.php to customize middleware behavior and exception handling:

File: bootstrap/app.php

PHP

<?php

declare(strict_types=1);

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up', // Native lightweight system health endpoint
    )
    ->withMiddleware(function (Middleware $middleware) {
        // LARAVEL 13 FEATURE: Expressive, centralized middleware configuration
        $middleware->trustProxies(at: '*');
        
        $middleware->validateCsrfTokens(except: [
            // List exempted webhook URIs if necessary
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // Custom exception handling customization
        $exceptions->render(function (\Throwable $e, Request $request) {
            if ($request->is('api/*')) {
                return response()->json([
                    'error' => 'Application exception encountered.',
                    'message' => $e->getMessage(),
                ], 500);
            }
        });
    })->create();

Chapter 10. Frontend Architecture: Blade Layouts & Dynamic Components

Now, build the user interface using Blade templates and dynamic components styled with Tailwind CSS via CDN.

Create the base layout file first:

File: resources/views/layouts/app.blade.php

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ $title ?? 'TaskPulse - Laravel 13 Masterclass' }}</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 font-sans text-gray-900 antialiased min-h-screen">
    <nav class="bg-indigo-600 text-white shadow-md mb-8">
        <div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
            <a href="{{ route('tasks.index') }}" class="text-xl font-bold tracking-tight">TaskPulse (Laravel 13)</a>
            <span class="text-xs bg-indigo-800 px-2.5 py-1 rounded-full uppercase tracking-wider">Production Mode</span>
        </div>
    </nav>

    <main class="max-w-7xl mx-auto px-4">
        @if (session('success'))
            <div class="bg-emerald-100 border-l-4 border-emerald-500 text-emerald-700 p-4 mb-6 rounded shadow-sm" role="alert">
                <p class="font-medium">{{ session('success') }}</p>
            </div>
        @endif

        {{ $slot }}
    </main>
</body>
</html>

Now, create the main Task Index Blade template:

File: resources/views/tasks/index.blade.php

HTML

<x-layouts.app>
    <x-slot:title>Task Dashboard - TaskPulse</x-slot:title>

    <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
        <!-- Task Creation Form -->
        <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200 h-fit">
            <h2 class="text-lg font-semibold text-gray-800 mb-4">Create New Task</h2>
            <form action="{{ route('tasks.store') }}" method="POST" class="space-y-4">
                @csrf
                <div>
                    <label class="block text-sm font-medium text-gray-700">Task Title</label>
                    <input type="text" name="title" required value="{{ old('title') }}"
                           class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500">
                    @error('title') <span class="text-red-500 text-xs">{{ $message }}</span> @enderror
                </div>

                <div>
                    <label class="block text-sm font-medium text-gray-700">Description</label>
                    <textarea name="description" rows="3" 
                              class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500">{{ old('description') }}</textarea>
                </div>

                <div>
                    <label class="block text-sm font-medium text-gray-700">Priority Level</label>
                    <select name="priority" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border focus:ring-indigo-500 focus:border-indigo-500">
                        <option value="low">Low Priority</option>
                        <option value="medium" selected>Medium Priority</option>
                        <option value="high">High Priority</option>
                    </select>
                </div>

                <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md transition duration-150">
                    Submit Task
                </button>
            </form>
        </div>

        <!-- Task List & Audit View -->
        <div class="lg:col-span-2 space-y-4">
            <h2 class="text-lg font-semibold text-gray-800">Active Tasks & Deferred Audits</h2>
            
            @forelse ($tasks as $task)
                <div class="bg-white p-5 rounded-xl shadow-sm border border-gray-200 flex flex-col justify-between space-y-4">
                    <div class="flex justify-between items-start">
                        <div>
                            <h3 class="font-bold text-gray-900 text-base">{{ $task->title }}</h3>
                            <p class="text-sm text-gray-600 mt-1">{{ $task->description ?? 'No description provided.' }}</p>
                        </div>
                        <span class="px-2.5 py-1 text-xs font-semibold rounded-full uppercase
                            {{ $task->priority === 'high' ? 'bg-red-100 text-red-700' : ($task->priority === 'medium' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-700') }}">
                            {{ $task->priority }}
                        </span>
                    </div>

                    <div class="flex items-center justify-between border-t border-gray-100 pt-3">
                        <span class="text-xs text-gray-500">Status: <strong class="uppercase text-gray-800">{{ $task->status }}</strong></span>
                        
                        <form action="{{ route('tasks.update-status', $task) }}" method="POST" class="flex items-center space-x-2">
                            @csrf
                            @method('PATCH')
                            <select name="status" onchange="this.form.submit()" class="text-xs rounded border-gray-300 p-1 border">
                                <option value="pending" {{ $task->status === 'pending' ? 'selected' : '' }}>Mark Pending</option>
                                <option value="in_progress" {{ $task->status === 'in_progress' ? 'selected' : '' }}>In Progress</option>
                                <option value="completed" {{ $task->status === 'completed' ? 'selected' : '' }}>Completed</option>
                            </select>
                        </form>
                    </div>

                    <!-- Audit Log List -->
                    <div class="bg-slate-50 p-3 rounded-lg border border-slate-200/60">
                        <h4 class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Deferred Audit History</h4>
                        <ul class="space-y-1">
                            @forelse ($task->auditLogs as $log)
                                <li class="text-xs text-slate-600 flex justify-between">
                                    <span>• {{ $log->details }}</span>
                                    <span class="text-slate-400">{{ $log->created_at->diffForHumans() }}</span>
                                </li>
                            @empty
                                <li class="text-xs text-slate-400 italic">No audit events registered yet.</li>
                            @endforelse
                        </ul>
                    </div>
                </div>
            @empty
                <div class="bg-white p-8 text-center rounded-xl border border-gray-200 text-gray-500">
                    No tasks found in system. Create one on the left!
                </div>
            @endforelse

            <div class="mt-4">
                {{ $tasks->links() }}
            </div>
        </div>
    </div>
</x-layouts.app>

Chapter 11. Automated Testing: Testing Business Logic with Pest 3

Robust applications require automated testing pipelines. Laravel 13 ships with native support for Pest 3. Let's write an automated test to verify our task creation endpoint and deferred audit logging.

Run the artisan command to generate a Pest feature test:

Bash

php artisan make:test TaskCreationTest

Edit the generated test file:

File: tests/Feature/TaskCreationTest.php

PHP

<?php

declare(strict_types=1);

use App\Models\Task;
use function Illuminate\Foundation\Testing\refreshDatabase;

uses(refreshDatabase::class);

test('user can create a task via HTTP endpoint and defer audit logs', function () {
    $payload = [
        'title' => 'Complete Laravel 13 Masterclass',
        'description' => 'Write production ready tutorial code with strict typing.',
        'priority' => 'high',
    ];

    $response = $this->post(route('tasks.store'), $payload);

    $response->assertRedirect(route('tasks.index'));
    
    // Assert task exists in database
    $this->assertDatabaseHas('tasks', [
        'title' => 'Complete Laravel 13 Masterclass',
        'priority' => 'high',
        'status' => 'pending',
    ]);

    // Assert deferred audit log was persisted
    $task = Task::first();
    $this->assertDatabaseHas('audit_logs', [
        'task_id' => $task->id,
        'action' => 'created',
    ]);
});

Execute your test suite from the terminal:

Bash

php artisan test

Chapter 12. Production Deployment & Performance Optimization

Before deploying your Laravel 13 application to a live server or managed cloud platform, execute these optimization commands to cache configuration, routes, and views:

Bash

# 1. Clear development caches
php artisan config:clear
php artisan route:clear
php artisan view:clear

# 2. Compile production deployment caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# 3. Optimize Composer Autoloader
composer install --optimize-autoloader --no-dev

Essential Production Server Configuration Check

Environment Safety:

Ensure APP_DEBUG=false in your production .env file to prevent sensitive stack traces from leaking to public users.

OPcache:

Enable the PHP OPcache extension in php.ini to store precompiled script bytecodes directly in shared RAM.

Process Manager:

Use Systemd or Supervisor to keep server processes active continuously.

Chapter 13. Comparative Matrix Table: Traditional Laravel vs. Modern Laravel 13

Architectural Feature

Traditional Laravel Paradigm

Modern Laravel 13 Framework Architecture

Directory Footprint

Complex nested directory trees (Kernel.php, Handler.php).

Clean, consolidated startup logic centralized inside bootstrap/app.php.

Deferred Tasks

Required configuring external queue drivers (Redis/SQS) and workers.

Native defer() helper defers non-blocking background tasks instantly.

PHP Requirements

Relaxed typing; legacy scalar type coercions permitted.

Enforces strict PHP 8.3/8.4 typing, typed class constants, and property hooks.

Route Processing

Dynamic route loading on uncached requests.

Pre-compiled route trees with optimized resolution algorithms.

Testing Paradigm

Default PHPUnit tests with heavy boilerplate assertions.

Expressive, elegant testing suites with native Pest 3 support.

Chapter 14. Frequently Asked Questions (FAQ)

❓What makes bootstrap/app.php important in Laravel 13?

In Laravel 13, bootstrap/app.php acts as the single configuration hub for the application. It replaces legacy kernel files (Http/Kernel.php, Console/Kernel.php) and exception handlers, allowing developers to configure routing, middleware pipelines, proxies, rate limiters, and custom exception renderers using a clean, fluent builder pattern.

❓How does the defer() function improve application performance?

The defer() helper allows execution of non-critical tasks (such as audit logging, analytics tracking, or webhook dispatching) after the HTTP response has already been sent to the client browser. This ensures end users receive immediate page responses without waiting for background operations to finish.

❓Is SQLite fully production-ready for Laravel 13 applications?

Yes! With recent improvements in SQLite 3 and modern hardware, SQLite is an exceptional choice for small to medium-sized production applications, microservices, and edge deployments due to its zero-configuration footprint, rapid execution speeds, and simple file-based backups.

❓Can I upgrade my existing Laravel app to Laravel 13 smoothly?

Yes. Laravel provides automated upgrade paths via tools like Laravel Shift. Because Laravel 13 maintains backwards compatibility for core Eloquent patterns and routing signatures, most application upgrades require minimal adjustments to custom middleware and configuration setups.

Komentar Pengunjung (0)

Belum ada komentar. Jadilah yang pertama memberikan komentar!