# Laravel 11: Building a Complete Application from Scratch > Comprehensive guide to building a Laravel 11 application with authentication, REST API, Eloquent ORM, and deployment. Practical tutorial for beginners and intermediate developers. - Published: 2026-01-14 - Updated: 2026-03-28 - Author: SharpSkill - Tags: laravel, php, rest api, eloquent, tutorial - Reading time: 15 min --- Laravel 11 redefines modern PHP development with a streamlined architecture and optimized performance. This major release removes many default configuration files, introduces a leaner structure, and significantly improves the developer experience. This guide walks through building a complete task management application, from installation to deployment. > **Laravel 11 Highlights** > > Laravel 11 adopts a minimalist structure: no more Kernel files, consolidated configuration in bootstrap/app.php, and a cleaner app/ directory. The framework requires PHP 8.2 minimum and natively integrates SQLite for quick startup. ## Installation and Project Setup Laravel 11 installs via Composer or the Laravel installer. The new project structure is leaner, with fewer configuration files to manage from the start. ```bash # terminal # Install the Laravel installer (recommended) composer global require laravel/installer # Create a new Laravel 11 project laravel new task-manager # Or directly with Composer composer create-project laravel/laravel task-manager # Navigate to the project cd task-manager ``` The installer offers several interactive options: choice of starter kit (Breeze, Jetstream), testing framework (Pest, PHPUnit), and database. ```bash # terminal # Start the development server php artisan serve # Server starts at http://localhost:8000 ``` ### Database Configuration Laravel 11 uses SQLite by default, ideal for development. For production applications, configuration for MySQL or PostgreSQL happens in the `.env` file. ```env # .env # MySQL configuration DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=task_manager DB_USERNAME=root DB_PASSWORD=secret # PostgreSQL configuration (alternative) # DB_CONNECTION=pgsql # DB_HOST=127.0.0.1 # DB_PORT=5432 # DB_DATABASE=task_manager # DB_USERNAME=postgres # DB_PASSWORD=secret ``` Database connection verification uses an Artisan command. ```bash # terminal # Verify database connection php artisan db:show # Run default migrations php artisan migrate ``` ## Creating Models and Migrations Eloquent ORM simplifies data manipulation with expressive models. The `make:model` command generates the model, migration, controller, and factory in one go. ```bash # terminal # Generate Task model with migration, factory, seeder, and controller php artisan make:model Task -mfsc # -m : migration # -f : factory # -s : seeder # -c : controller ``` This command creates four essential files for complete task management. ```php id(); // Relationship with the owner user $table->foreignId('user_id')->constrained()->cascadeOnDelete(); // Task title (required) $table->string('title'); // Detailed description (optional) $table->text('description')->nullable(); // Status: pending, in_progress, completed $table->string('status')->default('pending'); // Priority: low, medium, high $table->string('priority')->default('medium'); // Due date (optional) $table->date('due_date')->nullable(); // Completion marker $table->timestamp('completed_at')->nullable(); // Automatic timestamps (created_at, updated_at) $table->timestamps(); // Soft deletes for trash functionality $table->softDeletes(); // Indexes to optimize frequent queries $table->index(['user_id', 'status']); $table->index('due_date'); }); } public function down(): void { Schema::dropIfExists('tasks'); } }; ``` The migration defines the table structure with relationships, indexes, and soft delete to prevent accidental data loss. ```php 'date', 'completed_at' => 'datetime', ]; } // Constants for valid statuses public const STATUS_PENDING = 'pending'; public const STATUS_IN_PROGRESS = 'in_progress'; public const STATUS_COMPLETED = 'completed'; // Constants for priorities public const PRIORITY_LOW = 'low'; public const PRIORITY_MEDIUM = 'medium'; public const PRIORITY_HIGH = 'high'; // Relationship: a task belongs to a user public function user(): BelongsTo { return $this->belongsTo(User::class); } // Scope: pending tasks public function scopePending(Builder $query): Builder { return $query->where('status', self::STATUS_PENDING); } // Scope: in-progress tasks public function scopeInProgress(Builder $query): Builder { return $query->where('status', self::STATUS_IN_PROGRESS); } // Scope: completed tasks public function scopeCompleted(Builder $query): Builder { return $query->where('status', self::STATUS_COMPLETED); } // Scope: overdue tasks public function scopeOverdue(Builder $query): Builder { return $query->where('due_date', '<', now()) ->whereNull('completed_at'); } // Scope: tasks for a specific user public function scopeForUser(Builder $query, int $userId): Builder { return $query->where('user_id', $userId); } // Accessor: check if task is overdue public function getIsOverdueAttribute(): bool { return $this->due_date && $this->due_date->isPast() && !$this->completed_at; } // Method: mark as completed public function markAsCompleted(): void { $this->update([ 'status' => self::STATUS_COMPLETED, 'completed_at' => now(), ]); } } ``` The Task model uses Eloquent scopes for readable and reusable queries. Constants centralize valid values for statuses and priorities. > **Soft Deletes** > > The SoftDeletes trait adds a deleted_at column. Deleted tasks do not disappear from the database but are marked as deleted. Use withTrashed() to include them in queries. ## Setting Up Authentication with Breeze Laravel Breeze provides complete authentication with Blade views or an API. Installation takes a few minutes and generates everything needed: routes, controllers, views, and tests. ```bash # terminal # Install Laravel Breeze composer require laravel/breeze --dev # Install the Blade stack (traditional) php artisan breeze:install blade # Or for API only (SPA/mobile) php artisan breeze:install api # Compile assets npm install && npm run build # Run migrations (users, sessions tables, etc.) php artisan migrate ``` Breeze generates authentication routes, controllers, and views for registration, login, password reset, and email verification. ```php 'datetime', 'password' => 'hashed', ]; } // Relationship: a user owns multiple tasks public function tasks(): HasMany { return $this->hasMany(Task::class); } // Shortcut: user's pending tasks public function pendingTasks(): HasMany { return $this->tasks()->pending(); } // Shortcut: user's overdue tasks public function overdueTasks(): HasMany { return $this->tasks()->overdue(); } } ``` ## Creating Controllers and Routes The controller orchestrates business logic between HTTP requests and the model. Laravel 11 encourages single-action controllers or RESTful resources. ```php user() ->tasks() ->when($request->status, fn($q, $status) => $q->where('status', $status)) ->when($request->priority, fn($q, $priority) => $q->where('priority', $priority)) ->orderBy('due_date') ->orderBy('priority', 'desc') ->paginate(15); return view('tasks.index', compact('tasks')); } // Creation form public function create(): View { return view('tasks.create'); } // Store a new task public function store(Request $request): RedirectResponse { // Validate incoming data $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:5000', 'priority' => 'required|in:low,medium,high', 'due_date' => 'nullable|date|after_or_equal:today', ]); // Create the task linked to the user $request->user()->tasks()->create($validated); return redirect() ->route('tasks.index') ->with('success', 'Task created successfully.'); } // Display a task public function show(Task $task): View { // Verify user ownership Gate::authorize('view', $task); return view('tasks.show', compact('task')); } // Edit form public function edit(Task $task): View { Gate::authorize('update', $task); return view('tasks.edit', compact('task')); } // Update a task public function update(Request $request, Task $task): RedirectResponse { Gate::authorize('update', $task); $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:5000', 'status' => 'required|in:pending,in_progress,completed', 'priority' => 'required|in:low,medium,high', 'due_date' => 'nullable|date', ]); // Update completed_at if status = completed if ($validated['status'] === Task::STATUS_COMPLETED && !$task->completed_at) { $validated['completed_at'] = now(); } $task->update($validated); return redirect() ->route('tasks.index') ->with('success', 'Task updated.'); } // Delete a task public function destroy(Task $task): RedirectResponse { Gate::authorize('delete', $task); $task->delete(); return redirect() ->route('tasks.index') ->with('success', 'Task deleted.'); } // Mark as completed (quick action) public function complete(Task $task): RedirectResponse { Gate::authorize('update', $task); $task->markAsCompleted(); return back()->with('success', 'Task completed!'); } } ``` The controller uses Gates for authorization, built-in validation, and route model binding for concise and secure code. ### Defining Routes Routes define URLs and associate them with controller actions. Laravel 11 centralizes routes in `routes/web.php`. ```php group(function () { // Dashboard with statistics Route::get('/dashboard', function () { $user = auth()->user(); return view('dashboard', [ 'totalTasks' => $user->tasks()->count(), 'pendingTasks' => $user->tasks()->pending()->count(), 'completedTasks' => $user->tasks()->completed()->count(), 'overdueTasks' => $user->tasks()->overdue()->count(), ]); })->name('dashboard'); // RESTful routes for tasks Route::resource('tasks', TaskController::class); // Quick action to complete a task Route::patch('/tasks/{task}/complete', [TaskController::class, 'complete']) ->name('tasks.complete'); // Profile routes (generated by Breeze) Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); // Authentication routes (generated by Breeze) require __DIR__.'/auth.php'; ``` The `Route::resource()` method automatically generates seven standard RESTful routes: index, create, store, show, edit, update, and destroy. ## Setting Up Authorization Policies Policies centralize authorization logic for each model. They determine who can perform which actions on resources. ```bash # terminal # Generate the policy for Task php artisan make:policy TaskPolicy --model=Task ``` ```php is_admin) { return true; } return null; // Continue to specific methods } // Can view the list of tasks public function viewAny(User $user): bool { return true; // Any authenticated user } // Can view a specific task public function view(User $user, Task $task): bool { return $user->id === $task->user_id; } // Can create a task public function create(User $user): bool { return true; } // Can update a task public function update(User $user, Task $task): bool { return $user->id === $task->user_id; } // Can delete a task public function delete(User $user, Task $task): bool { return $user->id === $task->user_id; } // Can restore a soft-deleted task public function restore(User $user, Task $task): bool { return $user->id === $task->user_id; } // Can permanently delete public function forceDelete(User $user, Task $task): bool { return $user->id === $task->user_id; } } ``` Laravel automatically discovers policies through naming conventions. The `TaskPolicy` applies to the `Task` model. > **Policy Security** > > Policies are essential for security. Without them, a user could manipulate URLs to access other users' tasks. Always verify resource ownership. ## Building a REST API Laravel simplifies creating RESTful APIs with dedicated routes, JSON resources, and Sanctum for authentication. ```bash # terminal # Install Sanctum (included by default since Laravel 11) php artisan install:api # This command: # - Publishes Sanctum migrations # - Adds the HasApiTokens trait to the User model # - Configures API routes ``` ```php user() ->tasks() ->when($request->status, fn($q, $s) => $q->where('status', $s)) ->when($request->priority, fn($q, $p) => $q->where('priority', $p)) ->when($request->boolean('overdue'), fn($q) => $q->overdue()) ->latest() ->paginate($request->input('per_page', 15)); return TaskResource::collection($tasks); } // Create a task public function store(Request $request): JsonResponse { $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:5000', 'priority' => 'required|in:low,medium,high', 'due_date' => 'nullable|date|after_or_equal:today', ]); $task = $request->user()->tasks()->create($validated); return response()->json([ 'message' => 'Task created successfully.', 'data' => new TaskResource($task), ], 201); } // Display a task public function show(Task $task): TaskResource { $this->authorize('view', $task); return new TaskResource($task); } // Update a 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|max:5000', 'status' => 'sometimes|required|in:pending,in_progress,completed', 'priority' => 'sometimes|required|in:low,medium,high', 'due_date' => 'nullable|date', ]); if (isset($validated['status']) && $validated['status'] === Task::STATUS_COMPLETED && !$task->completed_at) { $validated['completed_at'] = now(); } $task->update($validated); return new TaskResource($task->fresh()); } // Delete a task public function destroy(Task $task): JsonResponse { $this->authorize('delete', $task); $task->delete(); return response()->json([ 'message' => 'Task deleted successfully.', ]); } } ``` ### API Resources for Data Transformation API resources transform Eloquent models into structured JSON responses, precisely controlling exposed data. ```php $this->id, 'title' => $this->title, 'description' => $this->description, 'status' => $this->status, 'priority' => $this->priority, 'due_date' => $this->due_date?->format('Y-m-d'), 'is_overdue' => $this->is_overdue, 'completed_at' => $this->completed_at?->toISOString(), 'created_at' => $this->created_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(), // Conditional inclusion of the user 'user' => $this->whenLoaded('user', fn() => [ 'id' => $this->user->id, 'name' => $this->user->name, ]), ]; } } ``` ### API Routes with Sanctum Authentication ```php user(); })->middleware('auth:sanctum'); // API routes protected by Sanctum Route::middleware('auth:sanctum')->group(function () { Route::apiResource('tasks', TaskController::class); // Dashboard statistics Route::get('/dashboard/stats', function (Request $request) { $user = $request->user(); return response()->json([ 'total' => $user->tasks()->count(), 'pending' => $user->tasks()->pending()->count(), 'in_progress' => $user->tasks()->inProgress()->count(), 'completed' => $user->tasks()->completed()->count(), 'overdue' => $user->tasks()->overdue()->count(), ]); }); }); ``` ## Automated Testing with Pest Laravel 11 integrates Pest PHP by default, an elegant and expressive testing framework. Tests ensure the application works correctly after each modification. ```php create(); $tasks = Task::factory()->count(5)->for($user)->create(); $response = $this->actingAs($user)->get('/tasks'); $response->assertOk(); $response->assertViewHas('tasks'); }); // Test: a user can create a task test('user can create a task', function () { $user = User::factory()->create(); $response = $this->actingAs($user)->post('/tasks', [ 'title' => 'New task', 'description' => 'Task description', 'priority' => 'high', 'due_date' => now()->addDays(7)->format('Y-m-d'), ]); $response->assertRedirect('/tasks'); $this->assertDatabaseHas('tasks', [ 'user_id' => $user->id, 'title' => 'New task', 'priority' => 'high', ]); }); // Test: title validation required test('task title is required', function () { $user = User::factory()->create(); $response = $this->actingAs($user)->post('/tasks', [ 'title' => '', 'priority' => 'medium', ]); $response->assertSessionHasErrors('title'); }); // Test: a user cannot view another user's task test('user cannot view another users task', function () { $user = User::factory()->create(); $otherUser = User::factory()->create(); $task = Task::factory()->for($otherUser)->create(); $response = $this->actingAs($user)->get("/tasks/{$task->id}"); $response->assertForbidden(); }); // Test: mark a task as completed test('user can complete a task', function () { $user = User::factory()->create(); $task = Task::factory()->for($user)->create(['status' => 'pending']); $response = $this->actingAs($user)->patch("/tasks/{$task->id}/complete"); $response->assertRedirect(); $task->refresh(); expect($task->status)->toBe('completed'); expect($task->completed_at)->not->toBeNull(); }); // API test: retrieve tasks test('api returns paginated tasks', function () { $user = User::factory()->create(); Task::factory()->count(20)->for($user)->create(); $response = $this->actingAs($user, 'sanctum') ->getJson('/api/tasks?per_page=10'); $response->assertOk() ->assertJsonCount(10, 'data') ->assertJsonStructure([ 'data' => [ '*' => ['id', 'title', 'status', 'priority', 'due_date'], ], 'links', 'meta', ]); }); ``` Running tests with Pest. ```bash # terminal # Run all tests php artisan test # Tests with code coverage php artisan test --coverage # Tests for a specific file php artisan test tests/Feature/TaskTest.php # Parallel tests (faster) php artisan test --parallel ``` ## Production Optimizations Before deployment, several optimizations improve Laravel application performance. ```bash # terminal # Cache configuration php artisan config:cache # Cache routes php artisan route:cache # Cache views php artisan view:cache # Optimize Composer autoloader composer install --optimize-autoloader --no-dev # Generate asset manifest npm run build ``` ### Production .env File Configuration ```env # .env.production APP_NAME="Task Manager" APP_ENV=production APP_DEBUG=false APP_URL=https://taskmanager.example.com # Cache and session via Redis (recommended) CACHE_DRIVER=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redis REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 # Database DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_DATABASE=task_manager_prod DB_USERNAME=app_user DB_PASSWORD=strong_password_here # Mail MAIL_MAILER=smtp MAIL_HOST=smtp.mailgun.org MAIL_PORT=587 MAIL_USERNAME=postmaster@taskmanager.example.com MAIL_PASSWORD=secret MAIL_ENCRYPTION=tls ``` > **Sensitive Variables** > > Never commit the .env file to the Git repository. Use .env.example as a template and configure sensitive variables directly on the production server or via secret managers. ## Conclusion Laravel 11 significantly simplifies modern PHP development with its streamlined structure and intelligent conventions. This guide covered the fundamentals for building a complete application: Eloquent models, authentication, RESTful controllers, authorization policies, API with Sanctum, and automated tests. ### Checklist for Quality Laravel Applications - ✅ Use migrations for all schema modifications - ✅ Implement policies to secure resource access - ✅ Validate all user input in controllers - ✅ Use API resources to control exposed data - ✅ Write tests for critical features - ✅ Configure cache and optimizations before deployment - ✅ Secure sensitive environment variables The Laravel ecosystem continues to evolve with packages like Livewire for reactive interfaces, Horizon for queue management, and Octane for exceptional performance. Mastering these fundamentals opens the door to robust and maintainable PHP applications. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/laravel/laravel-11-building-complete-application