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');