Laravel 11: Een complete applicatie bouwen vanaf nul

Uitgebreide handleiding voor Laravel 11: installatie, Eloquent-modellen, authenticatie met Breeze, controllers, routes, autorisatie met policies, REST API met Sanctum, testen met Pest en productie-deployment.

Laravel 11 tutorial: een complete applicatie bouwen vanaf nul

Laravel 11 introduceert een gestroomlijnde projectstructuur, native health checks en een vernieuwde configuratie op basis van bootstrap/app.php. Deze handleiding doorloopt het volledige traject: van installatie tot productie-deployment van een taakbeheer-applicatie.

Nieuw in Laravel 11

Laravel 11 verwijdert talloze boilerplate-bestanden (Http Kernel, Console Kernel, Middleware-directory) en introduceert een gecentraliseerde configuratie in bootstrap/app.php. Health-check routes, geleidelijke encryptiesleutel-rotatie en een vernieuwd starter kit-ecosysteem behoren tot de belangrijkste wijzigingen.

Installatie en projectconfiguratie

De Laravel-installer genereert het projectskelet in enkele seconden. Vereisten: PHP 8.2+ en Composer.

bash
# Install Laravel installer globally
composer global require laravel/installer

# Create a new Laravel 11 project
laravel new taskmanager

# Navigate to the project
cd taskmanager

Het .env-bestand configureert de databaseverbinding. Laravel 11 gebruikt standaard SQLite; voor productieomgevingen verdient PostgreSQL of MySQL de voorkeur.

env
# .env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=taskmanager
DB_USERNAME=postgres
DB_PASSWORD=secret

Na de configuratie worden de initiele migraties uitgevoerd en de ontwikkelserver gestart.

bash
# Run initial migrations
php artisan migrate

# Start the development server
php artisan serve

De applicatie is nu bereikbaar op http://localhost:8000.

Modellen en migraties aanmaken

Eloquent-modellen vormen de kern van elke Laravel-applicatie. Het volgende commando maakt gelijktijdig een model, migratie, factory en seeder aan.

bash
# Create model with migration, factory, and seeder
php artisan make:model Task -mfs

De migratie definieert de tabelstructuur. Belangrijk: softDeletes() maakt logisch verwijderen mogelijk zonder gegevensverlies.

database/migrations/xxxx_create_tasks_table.phpphp
public function up(): void
{
    Schema::create('tasks', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->string('title');
        $table->text('description')->nullable();
        $table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
        $table->enum('priority', ['low', 'medium', 'high'])->default('medium');
        $table->date('due_date')->nullable();
        $table->timestamps();
        $table->softDeletes();
    });
}

Het Eloquent-model declareert de fillable velden, casts en de relatie met User.

app/Models/Task.phpphp
namespace App\Models;

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

class Task extends Model
{
    use HasFactory, SoftDeletes;

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

    protected $casts = [
        'due_date' => 'date',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}
Soft Deletes in de praktijk

Soft Deletes markeren records als verwijderd zonder ze fysiek uit de database te verwijderen. Dit is bijzonder nuttig voor audit trails en gegevensherstel. Met Task::withTrashed() kunnen ook verwijderde records worden opgevraagd.

Authenticatie instellen met Breeze

Laravel Breeze biedt een lichtgewicht authenticatieoplossing met login, registratie, wachtwoord-reset en e-mailverificatie.

bash
# Install Breeze
composer require laravel/breeze --dev

# Scaffold with Blade stack
php artisan breeze:install blade

# Install frontend dependencies and build
npm install && npm run build

# Run migrations for auth tables
php artisan migrate

Het User-model wordt uitgebreid met de hasMany-relatie naar Tasks.

app/Models/User.phpphp
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    public function tasks(): HasMany
    {
        return $this->hasMany(Task::class);
    }
}

Klaar om je Laravel gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Controllers en routes aanmaken

Een resourceful controller implementeert alle CRUD-operaties. Laravel 11 maakt gebruik van de vereenvoudigde route-declaratie in routes/web.php.

app/Http/Controllers/TaskController.phpphp
namespace App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;

class TaskController extends Controller
{
    public function index(): View
    {
        $tasks = Auth::user()->tasks()
            ->orderBy('due_date')
            ->paginate(10);

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

    public function create(): View
    {
        return view('tasks.create');
    }

    public function store(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'in:pending,in_progress,completed',
            'priority' => 'in:low,medium,high',
            'due_date' => 'nullable|date|after:today',
        ]);

        Auth::user()->tasks()->create($validated);

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

    public function edit(Task $task): View
    {
        $this->authorize('update', $task);

        return view('tasks.edit', compact('task'));
    }

    public function update(Request $request, Task $task): RedirectResponse
    {
        $this->authorize('update', $task);

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'in:pending,in_progress,completed',
            'priority' => 'in:low,medium,high',
            'due_date' => 'nullable|date',
        ]);

        $task->update($validated);

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

    public function destroy(Task $task): RedirectResponse
    {
        $this->authorize('delete', $task);

        $task->delete();

        return redirect()->route('tasks.index')
            ->with('success', 'Task deleted successfully.');
    }
}

De routes worden geregistreerd als resource route, beschermd door de auth-middleware.

routes/web.phpphp
use App\Http\Controllers\TaskController;

Route::middleware(['auth', 'verified'])->group(function () {
    Route::resource('tasks', TaskController::class)
        ->except(['show']);
});

Autorisatie met policies instellen

Policies bundelen de autorisatielogica en zorgen ervoor dat gebruikers alleen hun eigen taken kunnen bewerken.

bash
# Generate a policy for the Task model
php artisan make:policy TaskPolicy --model=Task

De policy controleert of de geauthenticeerde gebruiker de eigenaar van de taak is.

app/Policies/TaskPolicy.phpphp
namespace App\Policies;

use App\Models\Task;
use App\Models\User;

class TaskPolicy
{
    public function update(User $user, Task $task): bool
    {
        return $user->id === $task->user_id;
    }

    public function delete(User $user, Task $task): bool
    {
        return $user->id === $task->user_id;
    }
}
Beveiliging bij policies

Policies moeten worden aangeroepen in elke controller-methode die gegevens wijzigt. Zonder autorisatiecontrole kunnen gebruikers via URL-manipulatie records van anderen bewerken of verwijderen. De methode $this->authorize() gooit automatisch een 403-exceptie wanneer de controle faalt.

Een REST API bouwen met Sanctum

Laravel Sanctum biedt lichtgewicht authenticatie voor SPA's en mobiele apps. Vanaf Laravel 11 is de API-configuratie met een enkel commando geregeld.

bash
# Install API support (includes Sanctum)
php artisan install:api

De API-controller retourneert JSON-responses via API Resources, die de datastructuur bepalen.

app/Http/Controllers/Api/TaskController.phpphp
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\TaskResource;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class TaskController extends Controller
{
    public function index(Request $request): AnonymousResourceCollection
    {
        $tasks = $request->user()->tasks()
            ->orderBy('created_at', 'desc')
            ->paginate(15);

        return TaskResource::collection($tasks);
    }

    public function store(Request $request): TaskResource
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'in:pending,in_progress,completed',
            'priority' => 'in:low,medium,high',
            'due_date' => 'nullable|date|after:today',
        ]);

        $task = $request->user()->tasks()->create($validated);

        return new TaskResource($task);
    }

    public function update(Request $request, Task $task): TaskResource
    {
        $this->authorize('update', $task);

        $validated = $request->validate([
            'title' => 'sometimes|required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'in:pending,in_progress,completed',
            'priority' => 'in:low,medium,high',
            'due_date' => 'nullable|date',
        ]);

        $task->update($validated);

        return new TaskResource($task);
    }

    public function destroy(Task $task)
    {
        $this->authorize('delete', $task);

        $task->delete();

        return response()->json(['message' => 'Task deleted'], 200);
    }
}

De API Resource bepaalt welke velden in de JSON-response worden opgenomen.

app/Http/Resources/TaskResource.phpphp
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class TaskResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'description' => $this->description,
            'status' => $this->status,
            'priority' => $this->priority,
            'due_date' => $this->due_date?->format('Y-m-d'),
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}

De API-routes worden beschermd door Sanctums auth:sanctum-middleware.

routes/api.phpphp
use App\Http\Controllers\Api\TaskController;

Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('tasks', TaskController::class);
});

Geautomatiseerd testen met Pest

Pest biedt een elegante syntax voor PHP-tests. De volgende tests dekken de CRUD-operaties en autorisatie.

tests/Feature/TaskTest.phpphp
use App\Models\Task;
use App\Models\User;

beforeEach(function () {
    $this->user = User::factory()->create();
});

test('user can create a task', function () {
    $response = $this->actingAs($this->user)->post('/tasks', [
        'title' => 'Test Task',
        'description' => 'Test description',
        'priority' => 'high',
    ]);

    $response->assertRedirect('/tasks');
    $this->assertDatabaseHas('tasks', [
        'title' => 'Test Task',
        'user_id' => $this->user->id,
    ]);
});

test('user can update own task', function () {
    $task = Task::factory()->create(['user_id' => $this->user->id]);

    $response = $this->actingAs($this->user)->put("/tasks/{$task->id}", [
        'title' => 'Updated Task',
        'status' => 'completed',
        'priority' => 'low',
    ]);

    $response->assertRedirect('/tasks');
    expect($task->fresh()->title)->toBe('Updated Task');
});

test('user cannot update another users task', function () {
    $otherUser = User::factory()->create();
    $task = Task::factory()->create(['user_id' => $otherUser->id]);

    $response = $this->actingAs($this->user)->put("/tasks/{$task->id}", [
        'title' => 'Hacked Task',
    ]);

    $response->assertForbidden();
});

test('user can delete own task', function () {
    $task = Task::factory()->create(['user_id' => $this->user->id]);

    $response = $this->actingAs($this->user)->delete("/tasks/{$task->id}");

    $response->assertRedirect('/tasks');
    $this->assertSoftDeleted('tasks', ['id' => $task->id]);
});

test('api returns paginated tasks', function () {
    Task::factory(20)->create(['user_id' => $this->user->id]);

    $response = $this->actingAs($this->user)->getJson('/api/tasks');

    $response->assertOk()
        ->assertJsonCount(15, 'data')
        ->assertJsonStructure([
            'data' => [['id', 'title', 'status', 'priority']],
            'meta' => ['current_page', 'last_page'],
        ]);
});

Het uitvoeren van de tests gebeurt met een enkel commando.

bash
# Run all tests
php artisan test

# Run with coverage
php artisan test --coverage

Productie-optimalisaties

Voor deployment verbeteren de volgende cache-commando's de prestaties aanzienlijk.

bash
# Cache configuration, routes, and views
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Optimize autoloader
composer install --optimize-autoloader --no-dev

Het .env.production-bestand bevat productiespecifieke instellingen.

env
# .env.production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://taskmanager.example.com

LOG_CHANNEL=stack
LOG_LEVEL=error

CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
Gevoelige omgevingsvariabelen

Het .env-bestand mag nooit in versiebeheer worden opgenomen. APP_KEY, databasewachtwoorden en API-sleutels dienen via beveiligde omgevingsvariabelen van de hosting provider te worden geconfigureerd. Het commando php artisan env:encrypt versleutelt het bestand voor veilige deployments.

Conclusie

Deze handleiding heeft de essentiiele bouwstenen van een Laravel 11-applicatie behandeld:

  • Projectconfiguratie: Laravel installer, omgevingsconfiguratie en initiele migraties
  • Eloquent-modellen: relaties, casts, soft deletes en factories voor gestructureerd gegevensbeheer
  • Authenticatie: Breeze levert login, registratie en wachtwoord-reset in enkele minuten
  • Controllers en routes: resourceful controllers en middleware-beschermde routegroepen
  • Autorisatie: policies garanderen dat gebruikers alleen hun eigen resources beheren
  • REST API: Sanctum-tokens en API Resources voor mobiele en SPA-clients
  • Tests: Pest-tests dekken CRUD-operaties en autorisatieregels
  • Productie: caching, autoloader-optimalisatie en beveiligde omgevingsconfiguratie

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Dagelijkse challenge

Zie jij de bug in Laravel?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 8 april 2026

Tags

#laravel
#php
#rest api
#eloquent
#tutorial

Delen

Gerelateerde artikelen