Laravel routing, middleware, and model binding
Status: Complete. Last reviewed 2026-08-27.
Precise mental model
Section titled “Precise mental model”Laravel routing turns an HTTP request into a selected endpoint plus route metadata. Middleware forms an ordered execution pipeline around that endpoint. Route binding turns selected URI values into typed values or application objects before the endpoint runs. None of those mechanisms, by itself, proves that the current actor may perform the requested operation.
Keep four decisions separate:
- A route matches request shape: method, host, path, and parameter constraints.
- Middleware handles request/response concerns that should wrap one or more endpoints.
- Binding resolves a matched parameter, often by querying an Eloquent model.
- A controller or action adapts the HTTP boundary to an application operation.
Validation, authorization, and tenant isolation are covered in the next bundle. They may execute in middleware or during binding, but they remain different responsibilities with different failure meanings.
Route registration and selection
Section titled “Route registration and selection”A route definition contributes an HTTP method, URI pattern, action, optional host, constraints, name, defaults, and middleware. Groups merge shared attributes such as a URI prefix, name prefix, domain, controller, and middleware. Resource routes generate a conventional set of route definitions; they do not create a new dispatch mechanism.
At dispatch, Laravel searches the registered route collection for a route matching the request. Static and dynamic patterns can overlap, so registration and constraints are part of correctness, not just style. A broad route such as /reports/{report} can capture a literal path intended for /reports/export if the concrete route is not given appropriate precedence. Supplemental routes should be registered before an overlapping resource route, and parameter constraints should express known shapes such as numeric IDs or accepted slugs.
Domain routing adds another matching dimension. Laravel 13 explicitly prioritizes routes with a domain over routes without one. That is a version-sensitive change from Laravel 12, where registration order between domain and non-domain routes could affect the winner. Within a route set, do not rely on accidental ordering when a constraint or distinct URI would make intent unambiguous.
Names decouple URL generation from the path. route('projects.show', $project) can survive a URI-prefix change when the route name and parameters remain stable. Names must be unique: duplicate names make URL generation and route caching ambiguous. Prefixes organize paths; name prefixes organize identifiers; neither creates an authorization boundary.
Use php artisan route:list -vv as evidence of the deployed graph, including expanded middleware. Source files show intent; route:list shows what the bootstrapped application registered. In production, also check whether the process is using a cached route collection.
Middleware composition and order
Section titled “Middleware composition and order”Global middleware wraps every request. Route middleware may come from groups, aliases, controller declarations, or the route itself. Laravel expands those declarations into concrete middleware and sorts priority-constrained middleware before building the pipeline. Therefore, textual order in one route file is not always the final execution order.
Each middleware’s handle() method receives the request and a $next closure. Code before $next($request) runs on the inward path. Code after it receives the downstream response and runs on the outward path in reverse order. If a middleware returns a response without calling $next, it short-circuits everything inside it: later middleware, binding if not already run, and the route action.
That ordering has production consequences:
- Authentication placed after expensive tenant discovery may permit unauthenticated requests to cause unnecessary database work.
- Rate limiting before authentication may group callers by IP; after authentication it can key by user, but cannot protect the authentication work itself.
- A transaction middleware wrapped around bindings does something different from one that begins after models were resolved.
- Response headers added by outer middleware can still appear on responses produced by inner short-circuits; headers added by skipped inner middleware cannot.
Middleware parameters configure reusable policies such as a role or guard, but long parameter strings become hidden configuration. Use an alias when it improves route readability; use a dedicated class when behavior has its own dependencies or failure modes. Do not put endpoint-specific business orchestration into middleware merely to keep a controller short.
In fresh Laravel 11–13 applications, global middleware, groups, aliases, and priority customization are configured through withMiddleware() in bootstrap/app.php. Older applications commonly use app/Http/Kernel.php. The file moved; the conceptual global stack, groups, aliases, and priority ordering did not.
Terminable middleware adds terminate(Request, Response), which Laravel calls after the response is sent. Laravel normally resolves a fresh middleware instance for termination; register it as a singleton only if the same instance is genuinely required. Termination is suitable for bounded cleanup or recording, not durable background work: it has no queue retry guarantee and still depends on runtime termination behavior.
How route model binding works
Section titled “How route model binding works”Route model binding runs after a route has matched, through the SubstituteBindings route middleware. Explicit binders are applied, then implicit binding uses the route’s parameters and the route action’s reflected types. For implicit Eloquent binding, the placeholder name must correspond to the typed action parameter: {project} and Project $project. Laravel queries the model using the route key and replaces the scalar route parameter with the resolved model.
If no matching model is found, Laravel converts the model-not-found failure into a 404 and the action is not invoked. A missing binding is therefore not a controller-level null case. Soft-deleted models are excluded by default; withTrashed() opts a route into resolving them. A route may customize missing-model behavior, but redirecting every miss can hide broken links or turn API 404s into misleading responses.
The default Eloquent route key is the primary key. A route can select a key inline, as in {post:slug}. In Laravel 13 a model may declare #[RouteKey('slug')]; older and current code can override getRouteKeyName(). Slugs used as route keys need the appropriate uniqueness and indexing guarantees. Binding by an unindexed or non-unique column turns convenience into slow or nondeterministic lookup.
String-backed enums can also be implicitly bound. A route with a typed backed enum action parameter runs only when the segment corresponds to a valid case; otherwise Laravel returns 404. This constrains a finite transport value without querying a model.
Use explicit Route::model() or Route::bind() when conventions cannot express resolution. A model can customize resolveRouteBinding() as well. These hooks are powerful but global custom behavior can surprise unrelated endpoints. Prefer the local {model:key} form when only one route needs a different lookup key.
Nested and scoped bindings
Section titled “Nested and scoped bindings”Consider /tenants/{tenant}/projects/{project}. Resolving Project globally by ID proves that the project exists, not that it belongs to the bound tenant. Scoped binding resolves the child through the parent’s relationship, so an unrelated child fails as a 404.
Laravel scopes a nested custom-key binding such as {post:slug} by convention through a relationship on the parent. scopeBindings() explicitly enables scoping when custom keys are not present; withoutScopedBindings() disables it when global lookup is intended. The inferred relationship name and custom binding behavior must still match the model design, so feature-test the actual parent/child combinations.
Scoped binding improves object-graph integrity and avoids exposing an unrelated resource at that nested URL. It is not authorization. A project can belong to the requested tenant while the current user still lacks permission to view it. Conversely, some privileged cross-tenant operation may intentionally use an unscoped administrative route and enforce access explicitly. Binding answers “which object does this URL identify?”; a policy answers “may this actor perform this ability?”
Failure timeline: route works locally but not after deployment
Section titled “Failure timeline: route works locally but not after deployment”- A release adds
/reports/exportand changes middleware on an existing resource route. - The production release keeps the previous cached route collection or long-running workers are not restarted as required by the deployment design.
- Requests continue to match the old graph, producing a 404, the wrong dynamic route, or the old middleware behavior.
- Reading the new route file looks correct, and local development without the route cache succeeds.
php artisan route:list -vvin the running release reveals the registered graph; checking the route cache and release path explains the discrepancy.- Rebuilding the route cache as a deployment artifact and switching releases coherently fixes the mismatch. Clearing caches manually on one host is not a fleet-wide deployment strategy.
php artisan route:cache reduces route-registration work by loading a generated route collection on each request. Any route change requires a fresh cache. Treat the cache as derived release state: build it from the same code and environment assumptions that will serve traffic, fail the deployment if route compilation fails, and avoid sharing a mutable cache file between releases.
Alternatives and decision triggers
Section titled “Alternatives and decision triggers”- Use a route constraint when rejecting a request shape does not require application state.
- Use enum binding for a finite string transport value.
- Use model binding when a route parameter directly identifies a persistence-backed model and automatic 404 semantics are appropriate.
- Use explicit binding when resolution is genuinely route infrastructure and conventions are insufficient.
- Resolve inside the action or application service when lookup requires operation-specific choices, multiple outcomes, or richer error semantics.
- Use middleware for cross-cutting behavior that wraps several endpoints and has clear ordering requirements.
- Use a separate action/controller method when behavior is specific to one operation.
Convenience is not the only criterion. Choose the layer that gives failures the correct meaning and leaves ordering visible enough to test.
Current and legacy context
Section titled “Current and legacy context”Current (Laravel 13): middleware configuration lives in bootstrap/app.php for fresh applications; #[RouteKey] can declare a model’s binding key; domain-bound routes are prioritized over non-domain routes; and route:list displays binding fields in current releases.
Common (Laravel 11–12): the streamlined bootstrap structure applies, but Laravel 12 retains older domain-route registration precedence. Inline binding keys, scoped bindings, enum binding, middleware groups, and route caching remain familiar.
Legacy: applications created before Laravel 11 commonly configure middleware in an HTTP kernel and may register routes through a RouteServiceProvider. Read the installed structure and upgrade guide rather than moving configuration only to resemble a fresh skeleton.
Interview practice
Section titled “Interview practice”- LARAVEL-ROUTING-01 — Trace route selection and dispatch
- LARAVEL-ROUTING-02 — Explain final middleware order
- LARAVEL-ROUTING-03 — Explain implicit model binding
- LARAVEL-ROUTING-04 — Separate scoped binding from authorization
- LARAVEL-ROUTING-05 — Diagnose a production-only routing failure
- LARAVEL-ROUTING-06 — Choose a boundary mechanism