Laravel Core

Route Standards

File Organization

routes/
├── web.php       # Web routes (primary)
├── auth.php      # Authentication routes (required in web.php)
├── api.php       # API routes (if needed)
└── console.php   # Artisan commands

Basic Structure

<?php

// routes/web.php
declare(strict_types=1);

use Illuminate\Support\Facades\Route;

// Public routes
Route::view('/', 'welcome');

// Authenticated routes
Route::middleware(['auth', 'verified'])->group(function (): void {
    Route::view('dashboard', 'dashboard')->name('dashboard');
    Route::view('profile', 'profile')->name('profile');
});

// Include auth routes
require __DIR__ . '/auth.php';

Route Definitions

View Routes

For simple pages without logic:

// Good - Direct view rendering
Route::view('/', 'welcome');
Route::view('/about', 'about')->name('about');
Route::view('/contact', 'contact')->name('contact');

// With data
Route::view('/terms', 'terms', ['version' => '1.0'])->name('terms');

Controller Routes

use App\Http\Controllers\TicketController;

// Resource routes
Route::resource('tickets', TicketController::class);

// Partial resource
Route::resource('tickets', TicketController::class)
    ->only(['index', 'show', 'store']);

Route::resource('tickets', TicketController::class)
    ->except(['destroy']);

// Single action controller
Route::get('/dashboard', DashboardController::class)->name('dashboard');

// Explicit routes
Route::get('/tickets', [TicketController::class, 'index'])->name('tickets.index');
Route::get('/tickets/{ticket}', [TicketController::class, 'show'])->name('tickets.show');
Route::post('/tickets', [TicketController::class, 'store'])->name('tickets.store');

Livewire Routes

use App\Livewire\Dashboard;
use App\Livewire\TicketList;
use App\Livewire\TicketCreate;

// Full-page Livewire components
Route::get('/dashboard', Dashboard::class)->name('dashboard');
Route::get('/tickets', TicketList::class)->name('tickets.index');
Route::get('/tickets/create', TicketCreate::class)->name('tickets.create');
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');

Naming Conventions

Route Names

Use snake_case or dot notation:

// Good
Route::get('/dashboard')->name('dashboard');
Route::get('/user/profile')->name('user.profile');
Route::get('/tickets')->name('tickets.index');
Route::get('/tickets/create')->name('tickets.create');
Route::get('/tickets/{ticket}')->name('tickets.show');
Route::get('/tickets/{ticket}/edit')->name('tickets.edit');

// Bad
Route::get('/dashboard')->name('Dashboard');      // PascalCase
Route::get('/tickets')->name('ticketsList');      // camelCase
Route::get('/tickets')->name('tickets-index');    // kebab-case

RESTful Naming

Verb URI Action Route Name
GET /tickets index tickets.index
GET /tickets/create create tickets.create
POST /tickets store tickets.store
GET /tickets/{ticket} show tickets.show
GET /tickets/{ticket}/edit edit tickets.edit
PUT/PATCH /tickets/{ticket} update tickets.update
DELETE /tickets/{ticket} destroy tickets.destroy

Middleware

Chaining Middleware

// Single middleware
Route::get('/dashboard')
    ->middleware('auth')
    ->name('dashboard');

// Multiple middleware
Route::get('/dashboard')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

// Group with middleware
Route::middleware(['auth', 'verified'])->group(function (): void {
    Route::get('/dashboard', Dashboard::class)->name('dashboard');
    Route::get('/tickets', TicketList::class)->name('tickets.index');
});

Common Middleware

// Authentication required
->middleware('auth')

// Must verify email
->middleware('verified')

// Guest only (login/register pages)
->middleware('guest')

// API authentication
->middleware('auth:sanctum')

// Combine for protected routes
->middleware(['auth', 'verified'])

Route Groups

By Middleware

// Authenticated routes
Route::middleware(['auth'])->group(function (): void {
    Route::view('dashboard', 'dashboard')->name('dashboard');
    Route::view('profile', 'profile')->name('profile');
});

// Verified users only
Route::middleware(['auth', 'verified'])->group(function (): void {
    Route::get('/tickets', TicketList::class)->name('tickets.index');
    Route::get('/settings', Settings::class)->name('settings');
});

By Prefix

Route::prefix('admin')->group(function (): void {
    Route::get('/dashboard', AdminDashboard::class)->name('admin.dashboard');
    Route::get('/users', UserList::class)->name('admin.users');
});

// Combined prefix and middleware
Route::prefix('admin')
    ->middleware(['auth', 'admin'])
    ->group(function (): void {
        Route::get('/dashboard', AdminDashboard::class)->name('admin.dashboard');
    });

By Name Prefix

Route::name('admin.')->group(function (): void {
    Route::get('/admin/dashboard', AdminDashboard::class)->name('dashboard');
    // Results in: admin.dashboard
});

Auth Routes

// routes/auth.php
<?php

declare(strict_types=1);

use App\Livewire\Auth\Login;
use App\Livewire\Auth\Register;
use App\Livewire\Auth\ForgotPassword;
use App\Livewire\Auth\ResetPassword;
use App\Livewire\Auth\VerifyEmail;
use Illuminate\Support\Facades\Route;

// Guest routes
Route::middleware('guest')->group(function (): void {
    Route::get('login', Login::class)->name('login');
    Route::get('register', Register::class)->name('register');
    Route::get('forgot-password', ForgotPassword::class)->name('password.request');
    Route::get('reset-password/{token}', ResetPassword::class)->name('password.reset');
});

// Authenticated routes
Route::middleware('auth')->group(function (): void {
    Route::get('verify-email', VerifyEmail::class)->name('verification.notice');

    Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
        ->middleware(['signed', 'throttle:6,1'])
        ->name('verification.verify');

    Route::post('logout', LogoutController::class)->name('logout');
});

Route Parameters

Basic Parameters

// Required parameter
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');

// Optional parameter
Route::get('/users/{user?}', UserProfile::class)->name('users.profile');

// Multiple parameters
Route::get('/teams/{team}/members/{member}', MemberShow::class)
    ->name('teams.members.show');

Route Model Binding

// Implicit binding (uses 'id' by default)
Route::get('/tickets/{ticket}', function (Ticket $ticket) {
    return view('tickets.show', compact('ticket'));
});

// Explicit binding key (model must have this column)
Route::get('/tickets/{ticket:hash_id}', TicketShow::class);

// Or define default key in model:
// public function getRouteKeyName(): string { return 'hash_id'; }

// Scoped binding
Route::get('/users/{user}/tickets/{ticket}', function (User $user, Ticket $ticket) {
    // $ticket is scoped to $user
})->scopeBindings();

Constraints

// Numeric only
Route::get('/tickets/{id}', TicketShow::class)->whereNumber('id');

// Alpha only
Route::get('/users/{name}', UserProfile::class)->whereAlpha('name');

// Alpha-numeric
Route::get('/posts/{slug}', PostShow::class)->whereAlphaNumeric('slug');

// UUID
Route::get('/orders/{order}', OrderShow::class)->whereUuid('order');

// Custom regex
Route::get('/users/{id}', UserShow::class)->where('id', '[0-9]+');

Redirects

// Simple redirect
Route::redirect('/here', '/there');

// Permanent redirect (301)
Route::redirect('/here', '/there', 301);
Route::permanentRedirect('/here', '/there');

// Named route redirect
Route::get('/home', function () {
    return redirect()->route('dashboard');
});

Fallback Routes

// 404 fallback
Route::fallback(function () {
    return response()->view('errors.404', [], 404);
});

API Routes

// routes/api.php
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->group(function (): void {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });

    Route::apiResource('tickets', TicketController::class);
});

Best Practices

Keep Routes File Clean

// Good - Clear and scannable
Route::view('/', 'welcome');

Route::middleware(['auth', 'verified'])->group(function (): void {
    Route::get('/dashboard', Dashboard::class)->name('dashboard');
    Route::get('/tickets', TicketList::class)->name('tickets.index');
    Route::get('/profile', Profile::class)->name('profile');
});

require __DIR__ . '/auth.php';

Use Named Routes

Always name routes for maintainability:

// In routes
Route::get('/tickets', TicketList::class)->name('tickets.index');

// In views
<a href="{{ route('tickets.index') }}">Tickets</a>

// In controllers/actions
return redirect()->route('tickets.index');

Avoid Logic in Routes

// Bad - Logic in route file
Route::get('/tickets', function () {
    $tickets = Ticket::where('user_id', auth()->id())->get();
    return view('tickets.index', compact('tickets'));
});

// Good - Use controller or Livewire
Route::get('/tickets', TicketList::class)->name('tickets.index');