Backend Development

Getting Started with Laravel: A Beginner's Guide

Mayur Dabhi
Mayur Dabhi
February 16, 2026
12 min read

If you've written a PHP app by hand, you know how it goes. You start with a few files, then you're copying the same database connection code everywhere, building your own login system, and escaping output in some templates but forgetting in others. By the time you add password resets, you've written half a framework, and not a very good one.

Laravel takes care of that groundwork so you can focus on the app itself. This post walks through installing it, how the folders are laid out, and the basics of routing, controllers, Eloquent, Blade, validation and authentication. It ends with a few problems that trip up almost everyone on their first deploy.

Why Laravel?

It's one of the most popular PHP frameworks on GitHub, the docs are good, and when you get stuck, someone has usually hit the same problem and written about it. For a new developer, that last part matters more than any feature list.

What is Laravel?

Taylor Otwell released Laravel in 2011 as a free, open-source PHP framework. It follows the Model-View-Controller (MVC) pattern: routes send a request to a controller, the controller talks to models, and a view renders the response.

What you get out of the box:

User Controller Handles Requests Model Business Logic View Blade Templates DB Request Response

Laravel's MVC Architecture Flow

Prerequisites and Installation

You'll need these installed first. Version requirements move with each Laravel release (recent versions need PHP 8.2 or newer), so check the docs for the version you're installing:

Requirement Minimum Version Recommended
PHP 8.2 8.3+
Composer 2.0 Latest
Node.js 18.x 20.x+
Database MySQL 5.7 / PostgreSQL 10 MySQL 8.0 / PostgreSQL 15

Step-by-Step Installation

1

Install Composer

Composer is PHP's dependency manager. Download it from getcomposer.org or use your system's package manager.

2

Create New Laravel Project

Use the Laravel installer or Composer to create a fresh Laravel application.

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

# Create a new Laravel project
laravel new my-awesome-app

# OR use Composer directly
composer create-project laravel/laravel my-awesome-app

# Navigate to project directory
cd my-awesome-app

# Start the development server
php artisan serve
3

Configure Environment

The installer usually creates .env for you. If it's missing (say, after cloning a repo), copy .env.example to .env and fill in your database credentials.

4

Generate Application Key

Run php artisan key:generate to set APP_KEY in .env. Skip this on a cloned project and every page throws "No application encryption key has been specified."

Using Docker?

Laravel Sail runs the app in Docker with ./vendor/bin/sail up, along with MySQL, Redis and a few other services. It's handy if you don't want PHP and a database installed locally, but on macOS and Windows file access through Docker can feel slower than a native setup.

Understanding Laravel's Directory Structure

A fresh project has a lot of folders, but you'll spend most of your time in app/, routes/, resources/views/ and database/. Here's the overview. (Since Laravel 11, api.php and channels.php only appear after you run php artisan install:api or install:broadcasting.)

📁 my-laravel-app/ 📂 app/ Models, Controllers, Middleware, Providers 📂 config/ Application configuration files (app.php, database.php) 📂 database/ Migrations, Seeders, Factories 📂 resources/ Views (Blade), CSS, JavaScript, Lang files 📂 routes/ web.php, api.php, console.php, channels.php 📂 public/ Entry point (index.php), Assets, Uploads 📂 storage/ & vendor/ Cache, logs, dependencies 📂 tests/ Feature & Unit tests

Routing Basics

Every request starts at a route. A route maps a URL and HTTP method to a closure or a controller method, and that's where you decide what happens next.

Define routes in routes/web.php:

use Illuminate\Support\Facades\Route;

// Basic GET route
Route::get('/', function () {
    return view('welcome');
});

// Route to a controller
Route::get('/users', [UserController::class, 'index']);

// Different HTTP methods
Route::post('/users', [UserController::class, 'store']);
Route::put('/users/{id}', [UserController::class, 'update']);
Route::delete('/users/{id}', [UserController::class, 'destroy']);

Capture dynamic segments from the URL:

// Required parameter
Route::get('/user/{id}', function ($id) {
    return "User ID: " . $id;
});

// Optional parameter with default
Route::get('/user/{name?}', function ($name = 'Guest') {
    return "Hello, " . $name;
});

// Multiple parameters
Route::get('/posts/{post}/comments/{comment}', function ($postId, $commentId) {
    return "Post: $postId, Comment: $commentId";
});

// Regex constraints
Route::get('/user/{id}', function ($id) {
    return "User: " . $id;
})->where('id', '[0-9]+');

Name your routes for easy URL generation:

// Define named route
Route::get('/user/profile', [ProfileController::class, 'show'])
    ->name('profile');

// Generate URL to named route
$url = route('profile');

// Redirect to named route
return redirect()->route('profile');

// In Blade templates
<a href="{{ route('profile') }}">Profile</a>

// With parameters
Route::get('/user/{id}/profile', [ProfileController::class, 'show'])
    ->name('profile');

$url = route('profile', ['id' => 1]);

Group routes with shared attributes:

// Middleware group
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/settings', [SettingsController::class, 'index']);
});

// Prefix group
Route::prefix('admin')->group(function () {
    Route::get('/users', [AdminUserController::class, 'index']);
    // URL: /admin/users
});

// Combined attributes
Route::prefix('api')
    ->middleware('api')
    ->name('api.')
    ->group(function () {
        Route::get('/users', [ApiUserController::class, 'index'])
            ->name('users.index');
        // Name: api.users.index
    });

Controllers

Closures in routes/web.php get messy after a handful of routes. Controllers move that logic into classes, usually one per resource, so everything related to users lives in UserController.

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    /**
     * Display a listing of users.
     */
    public function index()
    {
        $users = User::all();
        return view('users.index', compact('users'));
    }

    /**
     * Show the form for creating a new user.
     */
    public function create()
    {
        return view('users.create');
    }

    /**
     * Store a newly created user.
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ]);

        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => bcrypt($validated['password']),
        ]);

        return redirect()
            ->route('users.show', $user)
            ->with('success', 'User created successfully!');
    }

    /**
     * Display the specified user.
     */
    public function show(User $user)
    {
        return view('users.show', compact('user'));
    }
}
Artisan Tip

php artisan make:controller UserController --resource creates a controller with empty index, create, store, show, edit, update and destroy methods. Pair it with Route::resource('users', UserController::class) and you get all seven routes in one line.

Eloquent ORM: Working with Databases

Writing raw SQL for every insert and update gets old fast. With Eloquent, Laravel's ORM, each table gets a Model class, and you work with rows as PHP objects instead of arrays.

PHP Code User::find(1) $user->posts Eloquent Query Builder Builds SQL queries PDO Database MySQL/PostgreSQL SQLite/SQL Server Returns Model instances / Collections

How Eloquent processes database queries

app/Models/User.php
<?php

namespace App\Models;

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

class User extends Model
{
    use HasFactory;

    // Mass assignable attributes
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    // Hidden from arrays/JSON
    protected $hidden = [
        'password',
        'remember_token',
    ];

    // Attribute casting
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    // Relationship: User has many posts
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    // Relationship: User belongs to many roles
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

Common Eloquent Operations

Eloquent Query Examples
// Retrieve all users
$users = User::all();

// Find by primary key
$user = User::find(1);
$user = User::findOrFail(1); // Throws 404 if not found

// Query with conditions
$activeUsers = User::where('status', 'active')
    ->where('age', '>', 18)
    ->orderBy('created_at', 'desc')
    ->get();

// First matching record
$admin = User::where('role', 'admin')->first();

// Create new record
$user = User::create([
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'password' => bcrypt('password'),
]);

// Update existing record
$user->update(['name' => 'Jane Doe']);

// Delete record
$user->delete();

// Soft delete (if enabled)
$user->delete(); // Marks as deleted
User::withTrashed()->get(); // Include soft deleted

// Eager loading relationships
$users = User::with(['posts', 'roles'])->get();

// Pagination
$users = User::paginate(15);
Watch for N+1 queries

Looping over User::all() and calling $user->posts inside the loop runs one extra query per user. It's invisible with 10 rows in development and painful with 10,000 in production. Use with() as shown above, and consider adding Model::preventLazyLoading(! app()->isProduction()) in AppServiceProvider so Laravel throws an exception when you forget.

Blade Templates

Views live in resources/views and use Blade, Laravel's templating language. It gives you short directives like @if and @foreach, layout inheritance, and automatic escaping. Blade files compile to plain PHP and get cached, so there's no real runtime cost.

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>@yield('title', 'My Laravel App')</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    <nav>
        @include('partials.navigation')
    </nav>

    <main>
        @yield('content')
    </main>

    <footer>
        @include('partials.footer')
    </footer>
</body>
</html>
resources/views/users/index.blade.php
@extends('layouts.app')

@section('title', 'All Users')

@section('content')
    <h1>Users</h1>

    {{-- Display success message --}}
    @if(session('success'))
        <div class="alert alert-success">
            {{ session('success') }}
        </div>
    @endif

    {{-- Loop through users --}}
    @forelse($users as $user)
        <div class="user-card">
            <h2>{{ $user->name }}</h2>
            <p>{{ $user->email }}</p>
            
            @if($user->isAdmin())
                <span class="badge">Admin</span>
            @endif
            
            <a href="{{ route('users.show', $user) }}">
                View Profile
            </a>
        </div>
    @empty
        <p>No users found.</p>
    @endforelse

    {{-- Pagination links --}}
    {{ $users->links() }}
@endsection

Blade Directives Quick Reference

Directive Purpose Example
{{ $var }} Echo escaped content {{ $user->name }}
{!! $var !!} Echo unescaped HTML {!! $post->content !!}
@if / @else Conditional statements @if($user) ... @endif
@foreach Loop through arrays @foreach($items as $item)
@forelse Loop with empty fallback @forelse ... @empty ... @endforelse
@auth / @guest Auth conditionals @auth Welcome! @endauth
@csrf CSRF token field <form>@csrf</form>
@method Form method spoofing @method('PUT')

Form Validation

Laravel has a few ways to validate input. The simplest is calling validate on the request. Once a controller gets crowded, I'd move the rules into a Form Request class (php artisan make:request StorePostRequest).

Form Validation Example
public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email',
        'body' => 'required|min:10',
        'category_id' => 'required|exists:categories,id',
        'tags' => 'array',
        'tags.*' => 'exists:tags,id',
        'publish_date' => 'nullable|date|after:today',
        'image' => 'nullable|image|mimes:jpg,png|max:2048',
    ]);

    // If validation fails, Laravel automatically redirects back
    // with errors. If it passes, continue with validated data.
    
    Post::create($validated);
    
    return redirect()->route('posts.index')
        ->with('success', 'Post created successfully!');
}
Important

Validate everything that comes from the client, including hidden fields and IDs. Pass $validated to create(), not $request->all(), so users can't slip in fields like is_admin. Validation doesn't escape output for you, though. That's Blade's {{ }} job.

Authentication

Don't write login from scratch. Breeze and Jetstream generate the controllers, views and routes for you, and they handle the details people get wrong, like hashing, throttling and session regeneration. (Laravel 12 also ships newer starter kits through laravel new. Breeze still works and is easy to read.)

Setting Up Authentication with Breeze
# Install Laravel Breeze
composer require laravel/breeze --dev

# Install Breeze scaffolding
php artisan breeze:install

# Choose your stack:
# - blade (Traditional server-side)
# - react (React with Inertia)
# - vue (Vue with Inertia)
# - api (API only)

# Install dependencies and build assets
npm install && npm run dev

# Run migrations
php artisan migrate

You end up with registration, login, password reset, email verification and password confirmation. It's all plain code in your project, so you can change any of it.

👤 User Login Form email + password Auth Validate & Hash Session Store user state 🏠 Home Laravel Authentication Flow Built-in CSRF protection, rate limiting, and secure password hashing

Artisan Commands You'll Use Daily

You'll run php artisan constantly, Laravel's CLI. These are the commands I reach for most:

Command Description
php artisan serve Start development server
php artisan make:model Post -mcr Create model with migration, controller, and resource
php artisan migrate Run database migrations
php artisan migrate:fresh --seed Fresh migration with seeders
php artisan db:seed Run database seeders
php artisan tinker Interactive REPL for Laravel
php artisan route:list List all registered routes
php artisan cache:clear Clear application cache
php artisan config:clear Clear configuration cache
php artisan optimize Cache config, routes, and views

Mistakes I See on the First Deploy

Most beginner Laravel apps work fine locally and break on the server for the same few reasons:

Before you go to production

  • Wrong document root: point the web server at public/, not the project root. Otherwise your .env file, with database passwords, may be downloadable.
  • Storage permissions: if you see "The stream or file storage/logs/laravel.log could not be opened in append mode: Failed to open stream: Permission denied", the web server user can't write to storage/ and bootstrap/cache/. Fix ownership rather than running chmod 777.
  • Debug mode left on: set APP_DEBUG=false and APP_ENV=production. The debug error page shows stack traces and environment details to anyone.
  • env() outside config files: after php artisan optimize or config:cache, .env isn't loaded anymore, so env('SOMETHING') in a controller returns null. Read values through config() instead.
  • Long work in the request: sending emails or resizing images inline makes pages slow. That's the point where queues are worth learning.

When wouldn't I pick Laravel? For a tiny API with two or three endpoints, a microframework or plain PHP is lighter. If your team already knows Node or Django well, switching just for Laravel rarely pays off. And if most of your app is a heavy single-page frontend, Laravel still works as the API, but you won't use Blade much. For a typical CRUD app with users, forms and a database, though, it saves a lot of work. Middleware, Sanctum for APIs, queues and feature tests are the next things worth learning.

Helpful Resources

Official Documentation: laravel.com/docs
Laracasts: laracasts.com (video tutorials)
Laravel News: laravel-news.com
Laravel Daily: laraveldaily.com

Start small: one model, one resource controller, a couple of Blade views. Once that clicks, the rest of the framework is mostly more of the same pattern.

Laravel PHP MVC Web Development Backend Tutorial Beginners
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.