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 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.
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.
# Install Laravel installer globally
composer global require laravel/installer
# Create a new Laravel 11 project
laravel new taskmanager
# Navigate to the project
cd taskmanagerHet .env-bestand configureert de databaseverbinding. Laravel 11 gebruikt standaard SQLite; voor productieomgevingen verdient PostgreSQL of MySQL de voorkeur.
# .env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=taskmanager
DB_USERNAME=postgres
DB_PASSWORD=secretNa de configuratie worden de initiele migraties uitgevoerd en de ontwikkelserver gestart.
# Run initial migrations
php artisan migrate
# Start the development server
php artisan serveDe 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.
# Create model with migration, factory, and seeder
php artisan make:model Task -mfsDe migratie definieert de tabelstructuur. Belangrijk: softDeletes() maakt logisch verwijderen mogelijk zonder gegevensverlies.
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.
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 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.
# 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 migrateHet User-model wordt uitgebreid met de hasMany-relatie naar Tasks.
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.
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.
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.
# Generate a policy for the Task model
php artisan make:policy TaskPolicy --model=TaskDe policy controleert of de geauthenticeerde gebruiker de eigenaar van de taak is.
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;
}
}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.
# Install API support (includes Sanctum)
php artisan install:apiDe API-controller retourneert JSON-responses via API Resources, die de datastructuur bepalen.
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.
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.
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.
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.
# Run all tests
php artisan test
# Run with coverage
php artisan test --coverageProductie-optimalisaties
Voor deployment verbeteren de volgende cache-commando's de prestaties aanzienlijk.
# Cache configuration, routes, and views
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Optimize autoloader
composer install --optimize-autoloader --no-devHet .env.production-bestand bevat productiespecifieke instellingen.
# .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=redisHet .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.
Tags
Delen
Gerelateerde artikelen

Symfony 7: API Platform en Best Practices
Volledige gids voor het bouwen van professionele REST API's met Symfony 7 en API Platform 4. State Providers, Processors, validatie en serialisatie uitgelegd met praktische voorbeelden.

NestJS: Een complete REST API vanaf nul bouwen
Stap-voor-stap handleiding voor het bouwen van een productieklare REST API met NestJS, TypeScript, Prisma en class-validator. CRUD, validatie, foutafhandeling en interceptors.