Laravel Routing Reference Guide
A Laravel routing reference guide covering route definitions, parameters, named routes, and route groups.
Laravel Reference
Basic routes
Routes live in routes/web.php (session/cookie-aware, for browsers) or routes/api.php (stateless, for APIs), and map an HTTP verb + URI to a closure or controller action. Since Laravel 11, both files (along with routes/console.php) are registered explicitly in bootstrap/app.php via ->withRouting(), rather than being auto-loaded — so a brand-new project only ships web.php until you opt in to the others.
Closures are fine for quick prototypes, but a controller reference ([PostController::class, 'index']) is almost always the better choice once a route does anything beyond returning a static view: it's testable in isolation, keeps routes/web.php short, and benefits from route caching (see below), which closures can't use.
Route::get('/posts', [PostController::class, 'index']);
Route::post('/posts', [PostController::class, 'store']);
Route::put('/posts/{post}', [PostController::class, 'update']);
Route::patch('/posts/{post}', [PostController::class, 'update']);
Route::delete('/posts/{post}', [PostController::class, 'destroy']);
// Matches multiple verbs, or any verb
Route::match(['get', 'post'], '/search', [SearchController::class, 'index']);
Route::any('/webhook', [WebhookController::class, 'handle']);
Route parameters
Route::get('/posts/{post}', [PostController::class, 'show']);
// Optional parameter — give the closure/method a default
Route::get('/posts/{post?}', function (?string $post = null) {
// ...
});
// Constrain a parameter to match a pattern
Route::get('/posts/{id}', [PostController::class, 'show'])
->where('id', '[0-9]+');
// Reusable constraint shortcuts
Route::get('/posts/{id}', [PostController::class, 'show'])->whereNumber('id');
Route::get('/posts/{slug}', [PostController::class, 'show'])->whereAlpha('slug');
Constraints matter more than they look: without one, /posts/{id} and /posts/{slug} registered as separate routes could both match the same URI, and Laravel resolves the ambiguity by picking whichever was registered first, not whichever "makes more sense." Adding a where() constraint (or defining route model binding on a specific column) removes the ambiguity outright, since a URI segment that doesn't satisfy the pattern simply won't match that route at all.
Named routes
Naming a route lets you generate its URL (or redirect to it) without hardcoding the path — used throughout this site, e.g. route('hash-generator'). This pays off the first time a URI changes: renaming /posts to /articles is a one-line change in the route definition, versus a find-and-replace across every view, controller, and mailable that linked to it.
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
route('posts.show', ['post' => 1]); // /posts/1
redirect()->route('posts.show', $post);
<a href="{{ route('posts.show', $post) }}">View</a>
Route groups
Share attributes — a prefix, a name prefix, middleware — across many routes at once, rather than repeating them. This site's own Laravel reference section is registered exactly this way.
Route::prefix('admin')->name('admin.')->middleware('auth')->group(function () {
Route::get('/users', [Admin\UserController::class, 'index'])->name('users.index');
Route::get('/settings', [Admin\SettingController::class, 'index'])->name('settings.index');
});
// Generates /admin/users named "admin.users.index", etc.
Route model binding
When a route parameter's name matches a type-hinted Eloquent model in the handler, Laravel resolves the instance automatically (404-ing if it isn't found) instead of you calling find() yourself.
Route::get('/posts/{post}', [PostController::class, 'show']);
public function show(Post $post)
{
return view('posts.show', compact('post'));
}
// Bind by a column other than the primary key
Route::get('/posts/{post:slug}', [PostController::class, 'show']);
Model binding also respects Eloquent's global scopes and soft deletes by default, so a soft-deleted or otherwise excluded record 404s the same way a missing one would — useful, but worth remembering if a route unexpectedly can't find a record that clearly still exists in the database.
Inspecting registered routes
php artisan route:list
php artisan route:list --path=posts
php artisan route:list --name=admin.
route:list is the fastest way to confirm a route actually registered the way you expect — particularly useful after adding a route group, since prefixes and name prefixes are easy to get wrong. Add --reverse to sort by URI instead of registration order, or -v to see the middleware attached to each route.
Fallback routes
A single Route::fallback() route runs when no other route matches the request, letting you render a custom 404 page or hand off to an SPA's client-side router. It must be the last route registered, since Laravel matches routes in the order they're defined and a fallback route is (by definition) a catch-all.
Route::fallback(function () {
return response()->view('errors.404', [], 404);
});
Route caching for production
On every request, Laravel normally has to parse routes/web.php and routes/api.php to build the route table. For an application with hundreds of routes this adds measurable overhead, so production deployments typically cache the compiled route table to a single file instead:
php artisan route:cache
php artisan route:clear # after the next deploy, before caching again
The main gotcha: a cached route file is built from closures serialized ahead of time, and Laravel can't serialize a closure. Any route defined with a closure instead of a controller reference will break route:cache, which is another good reason to prefer controller actions for anything beyond a trivial route. Remember to run route:clear (or re-run route:cache) after every deploy that changes routes — a stale cache silently keeps serving the old route table.