Laravel Breeze Starter Project

What Developers Can Learn From Laravel Breeze
Laravel · Learning Path

What Developers Can Learn From Laravel Breeze

Breeze is often installed as a quick way to get login and registration working, then never looked at again. That's a missed opportunity — it's also one of the clearest, smallest examples of how a real Laravel app is organized. This tutorial walks through four things worth studying inside it: the file structure, the auth model/controller/view pattern, email verification, and how to safely extend the dashboard it ships with.

1. File & folder structure

Breeze doesn't introduce a new architecture — it just fills in files that a normal Laravel app already has slots for. That's exactly why it's useful to study: it shows you what "idiomatic" Laravel actually looks like once auth is wired up.

app/ ├── Http/Controllers/Auth/ # one controller per auth action │ ├── AuthenticatedSessionController.php │ ├── RegisteredUserController.php │ ├── PasswordResetLinkController.php │ ├── NewPasswordController.php │ ├── EmailVerificationPromptController.php │ ├── VerifyEmailController.php │ ├── EmailVerificationNotificationController.php │ └── ConfirmablePasswordController.php ├── Models/ │ └── User.php # implements MustVerifyEmail here resources/views/ ├── auth/ # login, register, forgot-password... ├── layouts/ # app.blade.php, guest.blade.php, navigation ├── components/ # x-input, x-primary-button, x-guest-layout... ├── dashboard.blade.php └── profile/ # edit profile, update password, delete account routes/ ├── web.php # app routes, dashboard route lives here └── auth.php # every auth-related route, grouped together

What's worth noticing here:

  • One controller, one job. Instead of a fat AuthController, Breeze splits registration, login, password reset, and verification into separate single-purpose controllers. This is a pattern worth copying in your own features.
  • Routes are grouped by concern. All auth routes live in routes/auth.php and get pulled into web.php, keeping the main routes file readable as the app grows.
  • Blade components over duplicated markup. Things like <x-input> and <x-primary-button> show how to avoid copy-pasting the same input/button styling across every form.

2. Auth model, controllers, and views

Breeze is a compact, working example of Laravel's MVC flow end to end. Following one action — registration — through all three layers is the fastest way to internalize the pattern.

Model: app/Models/User.php

The model doesn't just hold data — it declares behavior through contracts:

class User extends Authenticatable implements MustVerifyEmail
{
    use HasFactory, Notifiable;

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

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

Three things to learn from this file: $fillable controls mass assignment safety, $hidden keeps sensitive fields out of JSON responses, and the casts() method automatically hashes passwords on save instead of requiring you to call Hash::make() everywhere.

Controller: RegisteredUserController

public function store(Request $request): RedirectResponse
{
    $request->validate([
        'name' => ['required', 'string', 'max:255'],
        'email' => ['required', 'email', 'unique:'.User::class],
        'password' => ['required', 'confirmed', Rules\Password::defaults()],
    ]);

    $user = User::create([
        'name' => $request->name,
        'email' => $request->email,
        'password' => Hash::make($request->password),
    ]);

    event(new Registered($user));
    Auth::login($user);

    return redirect(route('dashboard', absolute: false));
}

Notice the shape: validate → create → fire an event → redirect. The event(new Registered($user)) line is a good lesson in decoupling — the controller doesn't know or care that this event triggers an email; it just announces that something happened.

View: resources/views/auth/register.blade.php

<x-guest-layout>
    <form method="POST" action="{{ route('register') }}">
        @csrf
        <x-input-label for="name" :value="__('Name')" />
        <x-text-input id="name" name="name" type="text" required autofocus />
        <x-input-error :messages="$errors->get('name')" />
        {{-- email, password, password_confirmation follow the same pattern --}}
    </form>
</x-guest-layout>

Each field follows the same three-line pattern: label, input, error. Once you recognize it, adding a new field to any Breeze form (or your own forms) becomes mechanical instead of something to look up each time.

LayerFileResponsibility
Modelapp/Models/User.phpData rules, casts, contracts
ControllerAuth/RegisteredUserController.phpValidate, act, redirect
Viewauth/register.blade.phpPresent the form, show errors

3. Email verification

Breeze includes everything for email verification, but it's off by default until you opt in — which makes it a good case study in how Laravel uses contracts and events to add optional behavior without touching the framework itself.

  1. The contract switches it on. Adding implements MustVerifyEmail to the User model is the single line that tells Laravel this user type needs to verify their email before being treated as fully authenticated.
  2. An event triggers the notification. event(new Registered($user)) in the registration controller is already listening for exactly this. When the model implements MustVerifyEmail, Laravel's built-in listener sends the verification email automatically — no extra code needed.
  3. Middleware enforces it. Adding 'verified' alongside 'auth' on a route blocks access until email_verified_at is set:
    Route::get('/dashboard', function () {
        return view('dashboard');
    })->middleware(['auth', 'verified'])->name('dashboard');
  4. Signed URLs protect the verification link. The route that actually verifies the email uses signed middleware, so the link can't be tampered with or reused after the signature expires.
The pattern to take away: contract on the model + event on the action + middleware on the route. That same three-part shape shows up throughout Laravel for other opt-in behaviors, not just email verification.

4. Adding pages to the Breeze dashboard

The dashboard Breeze ships with is intentionally minimal — a single Blade view behind auth and verified middleware. Extending it is a good first real task because it forces you to touch routes, views, and (optionally) a controller together.

Step 1 — Add a route

// routes/web.php
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/dashboard', function () {
        return view('dashboard');
    })->name('dashboard');

    Route::get('/projects', [ProjectController::class, 'index'])
        ->name('projects.index');
});

Grouping new pages under the same middleware keeps them behind login and verification automatically.

Step 2 — Create a controller

php artisan make:controller ProjectController
class ProjectController extends Controller
{
    public function index()
    {
        return view('projects.index', [
            'projects' => Auth::user()->projects,
        ]);
    }
}

Step 3 — Build the view using Breeze's layout

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            Projects
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
                @foreach ($projects as $project)
                    <p>{{ $project->name }}</p>
                @endforeach
            </div>
        </div>
    </div>
</x-app-layout>

Reusing <x-app-layout> means your new page automatically gets the same navigation bar, container width, and background as the dashboard — you're not rebuilding the shell each time.

Step 4 — Add it to navigation

Open resources/views/layouts/navigation.blade.php and add a link next to the existing Dashboard item:

<x-nav-link :href="route('projects.index')" :active="request()->routeIs('projects.index')">
    Projects
</x-nav-link>

Breeze already computes :active state this way for "Dashboard" — matching that pattern keeps the nav bar's highlighting consistent for every page you add.

Tip If a new page needs its own form input or button styling, reach for Breeze's existing components (x-text-input, x-primary-button, x-input-label) instead of writing new Tailwind classes from scratch — it keeps the whole app visually consistent with almost no extra work.
Common mistake: forgetting the middleware group

If a new route is added directly in routes/web.php without putting it inside the same middleware(['auth', 'verified']) group (or repeating the middleware on the route itself), the page will be publicly accessible even to logged-out or unverified users.

Where to go from here

Once these four areas feel familiar, Breeze stops being a black box you installed once and becomes a reference implementation you can borrow patterns from — single-purpose controllers, contract-driven optional behavior, and layout components are all things you'll want in apps that have nothing to do with authentication.

A companion guide to Laravel Breeze · written for developers learning the framework's conventions