Laravel Routing Guide 2026: Routes, Groups, Model Binding, and Named Routes

Laravel routing maps an incoming URL and HTTP method to the code that handles it. You define routes in routes/web.php (browser pages with sessions and CSRF protection) or routes/api.php (stateless JSON endpoints). Each route points to a closure or controller method. Named routes, route groups, and route model binding help keep a growing application organized and readable.

Most developers learn Route::get() on day one and stop there. That's fine for a ten-page application. But when an app grows to 150 routes, the same habits lead to duplicated middleware, hard-coded URLs that break whenever a route changes, and controllers filled with findOrFail() calls.

This guide covers the Laravel routing toolkit in 2026: basic routes, parameters and constraints, named routes, groups, route model binding, resource routes, and debugging with php artisan route:list. Every example uses Laravel 11+ conventions, so it applies directly if you're using Laravel 13.

Key Takeaways

  • Routes live in routes/web.php and routes/api.php. Since Laravel 11, api.php is not included by default. Run php artisan install:api to create it.

  • Name every route you link to. route('orders.show', $order) continues to work when the URL changes, but a hard-coded /orders/12 does not.

  • Use groups to share prefixes, names, middleware, and controllers. One group can replace dozens of repeated chained calls.

  • Route model binding removes boilerplate. Type-hint Post $post, and Laravel fetches the record or automatically returns a 404.

  • php artisan route:list is your source of truth. Filter routes with --path, --name, and --method, and add -v to see middleware.

The developers featured in this article are illustrative composites based on common scenarios.

Where Laravel Routes Live

A fresh Laravel application registers its route files in bootstrap/app.php (see the official routing documentation):

// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php', // added by `php artisan install:api`
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->create();

Each file uses a different middleware group:

File

Middleware group

Use for

routes/web.php

web: sessions, cookies, CSRF

Blade pages, Inertia, Livewire, form posts

routes/api.php

api: stateless, /api prefix

JSON endpoints for mobile apps and SPAs

routes/console.php

None

Closure-based Artisan commands and scheduled tasks

API routes automatically receive the /api prefix. For example, Route::get('/users') in api.php responds at /api/users.

If you're building a full JSON backend, our Laravel API generator guide shows how to scaffold one quickly.

Basic Laravel Routes

Laravel provides a router method for each HTTP verb:

use App\Http\Controllers\OrderController;
use Illuminate\Support\Facades\Route;

Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
Route::put('/orders/{order}', [OrderController::class, 'update']);
Route::patch('/orders/{order}', [OrderController::class, 'update']);
Route::delete('/orders/{order}', [OrderController::class, 'destroy']);

// Several verbs, or all of them
Route::match(['get', 'post'], '/search', [SearchController::class, 'handle']);
Route::any('/webhook', [WebhookController::class, 'handle']);

POST, PUT, PATCH, and DELETE routes in routes/web.php require a CSRF token. A third-party webhook hitting a route there may receive a 419 error, so place webhook endpoints in routes/api.php or exclude them from CSRF verification.

Use a closure for quick experiments and a controller for anything you plan to keep:

Route::get('/hello', function () {
    return 'Hello, Laravel';
});

Shortcut Routes

Two helpers save you from creating a controller when a route only needs to redirect or render a view:

Route::redirect('/old-pricing', '/pricing');          // 302
Route::permanentRedirect('/old-blog', '/blog');       // 301
Route::view('/about', 'pages.about', ['team' => 12]); // render a Blade view

Use permanentRedirect() when changing URLs on a live website. A 301 redirect tells search engines to transfer ranking signals to the new URL.

HTML Forms and PUT, PATCH, DELETE

Browsers only submit forms using GET or POST. To call a PUT or DELETE route from Blade, spoof the method:

<form method="POST" action="{{ route('orders.destroy', $order) }}">
    @csrf
    @method('DELETE')
    <button type="submit">Cancel order</button>
</form>

Route Parameters and Constraints

Wrap dynamic segments in braces. Laravel passes them to your closure or controller in order:

Route::get('/users/{id}', function (string $id) {
    return "User {$id}";
});

Route::get('/posts/{post}/comments/{comment}', function (string $post, string $comment) {
    // ...
});

Add ? for an optional parameter and provide a default value for the variable:

Route::get('/reports/{year?}', function (?string $year = null) {
    return $year ?? now()->year;
});

Constrain Parameters with where()

Without constraints, /users/abc can reach the same code as /users/42. Laravel provides helpers for common patterns:

Route::get('/users/{id}', [UserController::class, 'show'])->whereNumber('id');
Route::get('/tags/{slug}', [TagController::class, 'show'])->where('slug', '[a-z0-9-]+'); // allows hyphens
Route::get('/invoices/{invoice}', [InvoiceController::class, 'show'])->whereUuid('invoice');
Route::get('/reports/{period}', [ReportController::class, 'show'])->whereIn('period', ['daily', 'weekly', 'monthly']);

// Or use a raw regular expression
Route::get('/archive/{year}', [ArchiveController::class, 'show'])->where('year', '[0-9]{4}');

A request that fails a constraint returns a 404, so invalid input never reaches your controller. To apply a pattern everywhere, set it once in AppServiceProvider:

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Route::pattern('id', '[0-9]+');
}

Laravel Named Routes

A named route gives a URL a stable identifier. You generate links using the name rather than the path:

Route::get('/account/billing', [BillingController::class, 'show'])->name('billing.show');
// Anywhere in PHP or Blade
$url = route('billing.show');                     // https://app.test/account/billing
$url = route('orders.show', ['order' => 42]);     // pass parameters
$url = route('orders.show', $order);              // or pass the model itself
return to_route('billing.show');                  // redirect by name

Here's why it matters. Priya ran a SaaS dashboard with around 80 hard-coded links, such as href="/account/billing". In March, the marketing team asked to move every account page under /settings.

Changing the routes took five minutes. Finding every broken link in Blade files, emails, and JavaScript took two days, and three bugs still made it to production. On her next project, she named every route from the beginning. The same URL change later took just one commit.

Check the Current Route

Named routes also make it easy to manage navigation state:

<a href="{{ route('admin.users.index') }}"
   @class(['active' => request()->routeIs('admin.users.*')])>
    Users
</a>

routeIs() accepts wildcards, so one check covers admin.users.index, admin.users.edit, and other matching routes.

Tired of wiring routes, controllers, and views by hand? StackMivo generates a complete Laravel app from a plain-English description, with named routes, controllers, models, and migrations already connected.

Laravel Route Groups

Groups share attributes across multiple routes. You can combine middleware, prefixes, name prefixes, controllers, and domains.

Middleware Groups

Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
});

Prefix and Name Prefix

Use prefix() for the URL and name() for the route name. They're often used together:

use App\Http\Controllers\Admin;

Route::prefix('admin')
    ->name('admin.')
    ->middleware(['auth', 'can:access-admin'])
    ->group(function () {
        Route::get('/users', [Admin\UserController::class, 'index'])->name('users.index');
        // URL: /admin/users    Name: admin.users.index
    });

Note the trailing dot in name('admin.'). Without it, you'd get adminusers.index.

Controller Groups

When several routes use the same controller, define the controller once:

Route::controller(OrderController::class)->group(function () {
    Route::get('/orders/{order}', 'show')->name('orders.show');
    Route::post('/orders', 'store')->name('orders.store');
});

Subdomain Routing

Multi-tenant applications can capture part of the hostname:

Route::domain('{account}.example.com')->group(function () {
    Route::get('/', function (string $account) {
        return "Workspace: {$account}";
    });
});

Register domain routes before your root-domain routes. Otherwise, a root route with the same path may match first.

Register a Middleware Alias

In Laravel 11 and later, custom middleware aliases belong in bootstrap/app.php, not the old Kernel.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
    ]);
})

Laravel Route Model Binding

Route model binding injects an Eloquent model instead of a raw ID. It's one of the biggest time-savers in Laravel routing.

Implicit Binding

Type-hint a model and use the same name in the URL segment and the variable:

use App\Models\Post;

Route::get('/posts/{post}', function (Post $post) {
    return view('posts.show', ['post' => $post]);
});

Laravel runs Post::where('id', $value)->firstOrFail() for you. If the record is missing, Laravel returns a 404. If the segment is {post} but the variable is $article, binding won't happen, so keep the names identical.

Before binding, a typical controller looks like this:

public function show(string $id)
{
    $post = Post::findOrFail($id);
    // ...
}

After binding, the lookup disappears:

public function show(Post $post)
{
    // $post is already loaded
}

Across a 40-controller application, that adds up to a lot of deleted code. Daniel's team counted 112 findOrFail() calls during a refactor of their booking platform. Switching to implicit binding removed every one. It also fixed two endpoints that used find() without a null check and returned 500 errors instead of 404s for missing records.

Bind by a Column Other Than id

Use {post:slug} for SEO-friendly URLs:

Route::get('/blog/{post:slug}', [PostController::class, 'show']);

To make slug the default key for a model everywhere, override getRouteKeyName():

// app/Models/Post.php
public function getRouteKeyName(): string
{
    return 'slug';
}

Scoped Bindings for Nested Resources

In /users/{user}/posts/{post}, you usually want the post to belong to that user. Add scopeBindings():

Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
    return $post;
})->scopeBindings();

Laravel then queries the post through $user->posts(). A post belonging to another user returns a 404. When a nested segment uses a custom key, such as {post:slug}, Laravel scopes the child automatically. This assumes a posts relationship exists on User.

Soft-Deleted Models, Custom 404s, and Enums

// Include soft-deleted records
Route::get('/posts/{post}', [PostController::class, 'show'])->withTrashed();

// Redirect instead of returning a 404 when the model is missing
Route::get('/locations/{location:slug}', [LocationController::class, 'show'])
    ->missing(fn () => to_route('locations.index'));

Backed PHP enums can also be bound. If Category is a string-backed enum, /categories/{category} with a Category $category type-hint only matches valid enum values. Any other value returns a 404.

Explicit Binding

For full control over resolution, register a binding in AppServiceProvider::boot():

Route::bind('user', function (string $value) {
    return User::where('username', $value)->firstOrFail();
});

Keep authorization out of binding logic. Binding finds the record, while a policy or can middleware decides whether the user has access. Mixing the two hides security rules where reviewers may not look.

Resource Routes

A resource route registers the standard CRUD routes in one line (resource controller documentation):

Route::resource('photos', PhotoController::class);

That one call creates seven routes:

Verb

URI

Action

Route name

GET

/photos

index

photos.index

GET

/photos/create

create

photos.create

POST

/photos

store

photos.store

GET

/photos/{photo}

show

photos.show

GET

/photos/{photo}/edit

edit

photos.edit

PUT/PATCH

/photos/{photo}

update

photos.update

DELETE

/photos/{photo}

destroy

photos.destroy

Narrow the routes when you don't need all seven:

Route::resource('photos', PhotoController::class)->only(['index', 'show']);

Route::apiResource('posts', Api\PostController::class);
// No create/edit form routes

Route::resource('users.comments', CommentController::class)->shallow();
// Nested resources with shallow URLs

Generate a matching controller with php artisan make:controller PhotoController --resource --model=Photo. Our php artisan make cheatsheet lists the flags that create the model, migration, factory, and resource controller in one command.

php artisan route:list: View and Debug Your Route List

Once an application has a few dozen routes, php artisan route:list is the fastest way to answer the question, "What URL hits what code?" It prints every registered route with its method, URI, name, and action.

php artisan route:list                    # every route
php artisan route:list -v                 # include middleware for each route
php artisan route:list --path=admin       # URIs containing "admin"
php artisan route:list --name=orders      # route names containing "orders"
php artisan route:list --method=POST      # only POST routes
php artisan route:list --except-vendor    # hide package routes (Telescope, Horizon, etc.)
php artisan route:list --only-vendor      # show only package routes

Run php artisan route:list --help to see every option available in your installed version.

Common Problems route Solves

  • "Route [x] not defined" errors. Search with --name to confirm the route exists, including any group prefix such as admin..

  • Wrong route matching. Laravel matches routes in the order they're registered. If /posts/create returns a post-not-found error, /posts/{post} may be registered above it. Move the static route first or constrain the parameter.

  • Missing middleware. Use -v to check that auth applies to every admin route. This is a quick security check before each release.

  • Duplicate URIs. Two packages or route files may register the same path. The full list makes these collisions easy to spot.

Sam inherited a Laravel 9 codebase with 340 routes and no documentation. Before touching anything, he ran php artisan route:list -v --except-vendor. Within an hour, he found four /admin routes missing the auth middleware. One of them exported customer data as CSV. A ten-minute fix closed a security gap that had been open for over a year.

Cache Routes in Production

Route registration runs on every request. Cache routes during deployment:

php artisan route:cache    # compile routes for faster registration
php artisan route:clear    # remove the cache

Run route:cache as part of php artisan optimize in your deployment script, not during local development. A stale cache can cause the classic "I added a route, but it returns 404" problem. When debugging, run route:clear first.

Advanced Laravel Routing Features

Fallback Routes

Catch any request that doesn't match another route. Define the fallback route last:

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

Rate Limiting

Define a limiter in AppServiceProvider and attach it using throttle:

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

// Inside AppServiceProvider::boot()
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
Route::middleware('throttle:api')->group(function () {
    Route::post('/messages', [MessageController::class, 'store']);
});

Rate-limit login, password reset, and any endpoint that sends email or SMS. These are common targets for abuse.

Signed URLs

For links in emails, such as unsubscribe or download links, generate a signed route (signed URL documentation). Laravel rejects any URL that has been modified:

use Illuminate\Support\Facades\URL;

$url = URL::temporarySignedRoute('unsubscribe', now()->addDays(7), ['user' => $user->id]);

Route::get('/unsubscribe/{user}', UnsubscribeController::class)
    ->name('unsubscribe')
    ->middleware('signed');

Single-Action Controllers

A controller with only an __invoke() method can be passed directly, as shown in the unsubscribe route above. Generate one with php artisan make:controller UnsubscribeController --invokable.

Laravel Routing Best Practices Checklist

  • Every route you link to has a name, using resource.action naming (such as orders.show).

  • Shared middleware, prefixes, and names are defined in groups instead of repeated on individual routes.

  • Parameters are constrained (whereNumber, whereUuid) or model-bound.

  • Nested resources use scopeBindings() so users can't access other users' records.

  • Static routes (/posts/create) are registered before wildcard routes (/posts/{post}).

  • Admin and account routes are checked with php artisan route:list -v before release.

  • Login, password reset, and messaging endpoints are rate-limited.

  • php artisan route:cache runs only in the deployment script.

  • Old URLs use Route::permanentRedirect() to preserve their SEO value.

<!-- TODO: embed a relevant YouTube video on Laravel routing (prefer StackMivo channel; otherwise an authoritative Laravel educator) -->

Frequently Asked Questions

What is routing in Laravel?

Laravel routing maps an HTTP method and URL to the code that handles the request, usually a controller method. You define routes in routes/web.php for browser pages and routes/api.php for JSON APIs.

How do I list all routes in Laravel?

Run php artisan route:list. Add -v to show middleware, --path=admin to filter by URI, --name=orders to filter by route name, and --except-vendor to hide package routes.

What is route model binding in Laravel?

Route model binding automatically loads an Eloquent model from a URL parameter. If your route is /posts/{post} and your controller type-hints Post $post, Laravel finds the post by ID or by a custom key, such as a slug, and returns a 404 if it doesn't exist.

Why doesn't my Laravel 11 app have a routes/api.php file?

Since Laravel 11, API routing is optional. Run php artisan install:api to create routes/api.php, register it in bootstrap/app.php, and install Laravel Sanctum.

How do named routes work in Laravel?

Add ->name('orders.show') to a route, then generate its URL with route('orders.show', $order). If the URL path changes later, links generated from the route name continue to work.

What's the difference between Route::resource and Route::apiResource?

Route::resource creates seven routes, including the create and edit form pages. Route::apiResource creates five routes and excludes the HTML form routes that JSON APIs don't need.

Conclusion

Good Laravel routing comes down to a few simple habits. Name every route, group routes that share middleware or a prefix, and let route model binding handle your lookups. Resource routes cover standard CRUD operations in one line, and php artisan route:list shows you exactly what your application exposes.

Start with your current project. Run php artisan route:list -v --except-vendor today and look for unnamed routes, repeated middleware, and unprotected admin paths. Fix those three issues, and your routes file will become something new teammates can read in minutes.

Generate a fully routed Laravel app with StackMivo, free to start →