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. Both route segments and query string values arrive already percent-decoded — the URL encoder/decoder is useful when building a link to send elsewhere instead.
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. The same binding works whether the handler is a closure or a controller method.
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']);
// A backed enum type-hint restricts the segment to its valid values automatically
Route::get('/posts/status/{status}', function (PostStatus $status) {
// ...
});
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. The slug generator is a quick way to check what a given title turns into before relying on it for a slug column. A route parameter type-hinted as a PHP backed enum works the same way — Laravel 404s automatically if the segment doesn't match one of the enum's cases, without a manual where() constraint.
Signed URLs
A signed URL carries a hash of its own query string, so the route can trust the request wasn't tampered with even without requiring the visitor to be logged in — the standard way to build a "click this link" flow like email verification or an unsubscribe link.
URL::temporarySignedRoute('unsubscribe', now()->addDays(7), ['user' => $user->id]);
URL::signedRoute('unsubscribe', ['user' => $user->id]); // never expires
Route::get('/unsubscribe', [UnsubscribeController::class, 'handle'])
->name('unsubscribe')
->middleware('signed'); // rejects the request if the signature doesn't match
Because the signature covers the full query string, appending or changing any parameter after the link was generated invalidates it — there's no need to store a one-time token in the database for flows like this.
Subdomain routing
Route::domain('{account}.example.com')->group(function () {
Route::get('/', [TenantController::class, 'show']);
});
The captured subdomain segment is passed into the route/controller exactly like a URI parameter, which is the basis for simple subdomain-per-tenant setups.
Redirect and view shorthands
For a route that doesn't need any controller logic at all:
Route::redirect('/old-path', '/new-path'); // 302 by default
Route::redirect('/old-path', '/new-path', 301); // permanent
Route::view('/about', 'pages.about');
Route::view('/about', 'pages.about', ['team' => $team]); // with data
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. See the Artisan commands guide for more built-in commands like this one.
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 serialised ahead of time, and Laravel can't serialise 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.