Skip to main content
Back to Blog

Laravel Tutorial for Beginners 2025: Step-by-Step Guide to Build Your First App

Laravel Tutorial for Beginners 2025: Step-by-Step Guide to Build Your First App – cover image

Learn Laravel from Scratch: A Beginner’s Guide for 2025

Build Your First App Step-by-Step

Welcome! If you're looking to build powerful, modern web applications in 2025, you've come to the right place. Laravel is a top-tier PHP framework known for its elegant syntax and robust features. This guide is designed for absolute beginners. We'll go from a blank folder to a functioning to-do list application. You'll be able to follow along just by copying and pasting the code. Let's start building!


Prerequisites: Your Development Toolkit

Before we begin, make sure you have the following installed on your system. This is the essential toolkit for any Laravel developer in 2025.

  • PHP 8.2+: Laravel 11 requires PHP 8.2 or higher for security and performance.
  • Composer 2.0+: The package manager for PHP.
  • Node.js & npm: Required for asset compilation with Vite.
  • A Database: We'll use SQLite for simplicity, but MySQL 8.0+ or PostgreSQL 13+ also work.
  • A Code Editor: Visual Studio Code with PHP extensions is recommended.
  • A Command Line / Terminal: You'll need this for running Artisan commands.

Step 1: Install Laravel & Configure Your Project

Laravel 11 is the latest LTS version as of 2025. We'll use the Laravel installer for the best experience:

# First, install the Laravel installer globally
composer global require laravel/installer

# Create a new Laravel 11 project
laravel new todo-app

Alternatively, you can use Composer directly:

composer create-project laravel/laravel todo-app

Navigate into your new project directory:

cd todo-app

Laravel 11 Updates

Laravel 11 comes with a streamlined directory structure. The app/Http/Kernel.php has been replaced with bootstrap/app.php for middleware configuration. The application key is automatically generated during installation.

Set up the database. Create an empty SQLite database file:

# On Windows
type nul > database/database.sqlite

# On macOS/Linux
touch database/database.sqlite

Update your .env file for SQLite:

DB_CONNECTION=sqlite
# Remove or comment out these lines:
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

Start the development server using Artisan:

php artisan serve

Visit http://127.0.0.1:8000 in your browser to see the Laravel welcome page.


Step 2: Create the Database Migration and Model

Laravel uses Migrations for database version control and Models for database interactions. Let's create both:

php artisan make:model Task -m

This creates both a model and migration. Open the migration file in database/migrations/ and update the up() method:

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->boolean('is_completed')->default(false);
            $table->timestamps();
        });
    }

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

Run the migration to create the table:

php artisan migrate

Step 3: Configure the Model and Create Routes

First, update the Task model in app/Models/Task.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    protected $fillable = [
        'title',
        'is_completed'
    ];

    protected $casts = [
        'is_completed' => 'boolean',
    ];
}

Create a resource controller:

php artisan make:controller TaskController --resource

In Laravel 11, routes are simplified. Add your routes to routes/web.php:

<?php

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

Route::get('/', [TaskController::class, 'index'])->name('tasks.index');
Route::resource('tasks', TaskController::class)->except(['show', 'edit']);

Step 4: Build the Controller Logic

Update app/Http/Controllers/TaskController.php with modern Laravel 11 syntax:

<?php

namespace App\Http\Controllers;

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

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

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

        Task::create($validated);

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

    public function update(Request $request, Task $task): RedirectResponse
    {
        $task->update([
            'is_completed' => !$task->is_completed
        ]);

        $message = $task->is_completed
            ? 'Task marked as completed!'
            : 'Task marked as pending!';

        return redirect()
            ->route('tasks.index')
            ->with('success', $message);
    }

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

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

Step 5: Create the Blade View

Create the directory structure and view file:

mkdir resources/views/tasks
touch resources/views/tasks/index.blade.php

Add this modern, responsive view to resources/views/tasks/index.blade.php:

@extends('layouts.app')

@section('content')
<div class="container max-w-2xl mx-auto px-4">
    <div class="bg-white rounded-2xl shadow-xl p-8">
        <div class="text-center mb-8">
            <h1 class="text-4xl font-bold text-gray-800 mb-2">My Tasks</h1>
            <p class="text-gray-600">Built with Laravel 11</p>
        </div>

        @if (session('success'))
        <div class="bg-green-50 border-l-4 border-green-400 p-4 mb-6 rounded-r-lg">
            <div class="flex">
                <div class="flex-shrink-0">
                    <svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
                        <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
                    </svg>
                </div>
                <div class="ml-3">
                    <p class="text-sm font-medium text-green-800">{{ session('success') }}</p>
                </div>
            </div>
        </div>
        @endif

        <form action="{{ route('tasks.store') }}" method="POST" class="mb-8">
            @csrf
            <div class="flex gap-3">
                <input type="text" 
                       name="title"
                       placeholder="Add a new task..." 
                       required
                       value="{{ old('title') }}"
                       class="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 @error('title') border-red-500 @enderror">
                <button type="submit" 
                        class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-xl transition-all duration-200 transform hover:scale-105 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
                    Add Task
                </button>
            </div>
            @error('title')
                <p class="text-red-500 text-sm mt-2">{{ $message }}</p>
            @enderror
        </form>

        <div class="space-y-3">
            @forelse ($tasks as $task)
                <div class="group flex items-center justify-between p-4 bg-gray-50 rounded-xl hover:bg-gray-100 transition-all duration-200">
                    <div class="flex items-center space-x-3 flex-1">
                        <div class="flex-shrink-0">
                            @if($task->is_completed)
                                <svg class="h-5 w-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
                                </svg>
                            @else
                                <div class="h-5 w-5 rounded-full border-2 border-gray-300"></div>
                            @endif
                        </div>
                        <span class="flex-1 {{ $task->is_completed ? 'line-through text-gray-500' : 'text-gray-800' }} transition-all duration-200">
                            {{ $task->title }}
                        </span>
                    </div>

                    <div class="flex space-x-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
                        <form action="{{ route('tasks.update', $task) }}" method="POST" class="inline">
                            @csrf
                            @method('PUT')
                            <button type="submit" 
                                    class="px-3 py-1 text-sm font-medium rounded-lg transition-all duration-200 {{ $task->is_completed ? 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200' : 'bg-green-100 text-green-800 hover:bg-green-200' }}"
                                    title="{{ $task->is_completed ? 'Mark as pending' : 'Mark as completed' }}">
                                {{ $task->is_completed ? 'Undo' : 'Done' }}
                            </button>
                        </form>

                        <form action="{{ route('tasks.destroy', $task) }}" method="POST" class="inline">
                            @csrf
                            @method('DELETE')
                            <button type="submit" 
                                    onclick="return confirm('Are you sure you want to delete this task?')"
                                    class="px-3 py-1 bg-red-100 hover:bg-red-200 text-red-800 text-sm font-medium rounded-lg transition-all duration-200"
                                    title="Delete task">
                                Delete
                            </button>
                        </form>
                    </div>
                </div>
            @empty
                <div class="text-center py-12">
                    <div class="w-24 h-24 mx-auto mb-4">
                        <svg class="w-full h-full text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
                        </svg>
                    </div>
                    <h3 class="text-lg font-medium text-gray-500 mb-1">No tasks yet</h3>
                    <p class="text-gray-400">Create your first task to get started!</p>
                </div>
            @endforelse
        </div>

        @if($tasks->count() > 0)
        <div class="mt-8 p-4 bg-gray-50 rounded-xl">
            <div class="flex justify-between text-sm text-gray-600">
                <span>Total: {{ $tasks->count() }} tasks</span>
                <span>Completed: {{ $tasks->where('is_completed', true)->count() }}</span>
                <span>Pending: {{ $tasks->where('is_completed', false)->count() }}</span>
            </div>
        </div>
        @endif
    </div>
</div>
@endsection

Now visit http://127.0.0.1:8000 to see your fully functional to-do app!


Conclusion and Next Steps

Congratulations! You've successfully built a modern Laravel 11 application from scratch. You've learned the fundamentals of MVC architecture, routing, controllers, models, migrations, and Blade templating.

Here's what you can explore next to continue your Laravel journey:

  • Form Request Validation: Use dedicated request classes for complex validation logic.
  • Authentication with Laravel Breeze: Add user registration, login, and password reset functionality.
  • Eloquent Relationships: Create relationships between models (User has many Tasks).
  • API Development: Build RESTful APIs using Laravel's API resources.
  • Job Queues: Handle time-consuming tasks asynchronously.
  • Testing: Write feature and unit tests using PHPUnit.

Frequently Asked Questions (FAQ)

What's new in Laravel 11?

Laravel 11 features a streamlined application structure, improved performance, per-second rate limiting, health check routing, graceful encryption key rotation, and enhanced Eloquent casts. It requires PHP 8.2+ and includes Vite for asset compilation by default.

Do I need to know PHP to learn Laravel?

Yes. A solid understanding of PHP fundamentals, object-oriented programming (OOP), namespaces, and composer dependency management is essential before diving into Laravel.

Is Laravel still relevant in 2025?

Absolutely. Laravel continues to evolve with regular updates, maintains a massive community, offers excellent documentation, and provides a rich ecosystem including Laravel Forge, Vapor, and Nova. It's widely used by companies of all sizes.

What is Artisan in Laravel?

Artisan is Laravel's powerful command-line interface that provides numerous helpful commands for development. It can generate boilerplate code, run migrations, clear caches, run tests, and much more, significantly speeding up development workflow.