How does Laravel's Route::domain() serve a franchise subdomain such as {franchise}.example.com, and what changed about domain routes in Laravel 13?
answer
- host pattern with a parameter
- domain values passed before path values
- route() needs the domain parameter
- domain routes checked first in 13
- route:list --domain
basics
~10 sRoute::domain('{franchise}.example.com')->group(...) matches the host and captures franchise, which reaches the handler before path parameters; route() needs it to build URLs, and since Laravel 13 domain routes are matched before routes without a domain.
solid answer
~40 s`Route::domain()` sets a host pattern on a group. The router compiles it like a path, so `{franchise}` in `{franchise}.example.com` is captured from `north.example.com`, and a `where('franchise', '[a-z0-9-]+')` constraint applies to it too. Domain values are merged **ahead of** path values, so the action is `function (string $franchise, string $order)`. URL generation needs the value: `route('admin.orders.show', ['franchise' => 'north', 'order' => 7])` builds `https://north.example.com/...`; without it `route()` throws `UrlGenerationException`. The Laravel 13 change: routes with an explicit domain are now matched **before** routes without one, whatever the registration order, so a catch-all main-site route no longer steals subdomain requests. Apps that relied on the old registration-order precedence must re-check their matching.
code
php · 14 lines<?php
namespace App\Http\Controllers;
use Illuminate\View\View;
class OrderController extends Controller
{
// Route: Route::domain('{franchise}.example.com')->...->get('/orders/{order}', ...)
public function show(string $franchise, string $order): View
{
return view('franchise.orders.show', compact('franchise', 'order'));
}
}go deeper
Know that Route::domain() matches the host and can capture a part of it as a parameter.
Explain that host values come before path values in the action, and that route() needs the host value to build the URL.
Account for Laravel 13's domain-first matching when upgrading, constrain host parameters, and plan reserved subdomains.
Weigh subdomain-per-tenant routing against path-based tenancy, including DNS, cookies and certificates, before committing to one.
## Routing on the host Most routes care only about the path. **Domain routing** also matches the request's host, which lets one app serve several hosts: a public site on `example.com` and a dashboard for each franchise on `north.example.com`, `south.example.com` and so on. You attach the host pattern to a group: ```php use App\Http\Controllers\DashboardController; use Illuminate\Support\Facades\Route; Route::domain('{franchise}.example.com') ->name('franchise.') ->group(function () { Route::get('/dashboard', [DashboardController::class, 'show'])->name('dashboard'); }); ``` ## How the franchise value reaches the action The router compiles the host pattern into its own regex. When a request for `north.example.com/dashboard` arrives: 1. The path regex matches `/dashboard`. 2. The host regex matches `north.example.com` and captures `franchise = north`. 3. The parameter binder merges host values **before** path values. 4. Values are passed to the action by position. So for `/orders/{order}` inside the group, the action signature is `show(string $franchise, string $order)`, not the other way round. Constraints work on host parameters as on path ones: `->where('franchise', '[a-z0-9-]+')` rejects hosts such as `www_old.example.com` before any code runs. ## Generating URLs to domain routes `route()` must fill the host placeholder too: ```php route('franchise.dashboard', ['franchise' => 'north']); // https://north.example.com/dashboard ``` - Without the `franchise` value the generator throws `UrlGenerationException` (`Missing parameter: franchise`). - The result is always an absolute URL on the right host, which is exactly what cross-subdomain links need. - Repeating `'franchise' => ...` in every call gets tedious; request-wide defaults for URL parameters are a separate feature of the URL generator. ## The Laravel 13 precedence change The route collection now keeps **domain routes in their own bucket and checks them first**. The upgrade guide describes it as: routes with an explicit domain are now prioritised before non-domain routes in route matching. Why it matters for this dashboard: | Situation | Before Laravel 13 | Laravel 13 | |---|---|---| | Main site registers `/dashboard` (no domain) **before** the franchise group | the main-site route could win for `north.example.com/dashboard`, by registration order | the franchise domain route wins | | A catch-all non-domain route registered early | could swallow subdomain requests | domain routes are tried first | The guide rates the impact as low but tells apps that relied on the old registration precedence to review their route matching. After upgrading, check hosts that are served by both kinds of route. ## Using the franchise value beyond the action Most franchise dashboards need the current franchise everywhere, not just in one action: - Route middleware can read it with `$request->route('franchise')`, load the franchise record, and abort with 404 for unknown hosts before any controller runs. - Views and services can receive the loaded record from that middleware instead of repeating the lookup. - Keeping the lookup in one place also gives a single spot to enforce reserved subdomains and inactive franchises. ## Local development and deployment notes - Every franchise host must resolve to the app (a wildcard DNS record in production; a local host mapping in development). - The same app key and session settings apply to every host; whether sessions should be shared across subdomains is a session-configuration decision, not a routing one. - `php artisan route:list --domain=example.com` shows which routes are bound to host patterns. ## Domain routes in route:list `php artisan route:list` prints a route bound to a host pattern with the pattern in front of its URI (`{franchise}.example.com/dashboard`), `--json` gives it a separate `domain` field, and `--domain=` filters on it. After an upgrade to Laravel 13, listing the paths that exist both with and without a domain is the quickest way to see which requests change owner under the new precedence. ## Pitfalls interviewers probe - **Parameter order**: the domain value comes first, so `show(string $order, string $franchise)` receives them swapped. - **Forgetting the host value in `route()`**, which throws at render time. - **Assuming registration order decides** between a domain route and a plain route in Laravel 13. - **Nesting a second `domain()`**: an inner group's domain replaces the outer one rather than combining with it.
- After upgrading to Laravel 13, requests to north.example.com/dashboard reach a different controller than before. What changed?The route collection now checks routes with an explicit domain before routes without one. If the main site registered a plain `/dashboard` route before the franchise domain group, it used to win by registration order; in Laravel 13 the franchise route wins. Review overlapping paths and make the intended precedence explicit.
- How do you stop hosts like admin.example.com from being treated as a franchise?Constrain the host parameter with `->where('franchise', ...)` so reserved labels fail it, or register the reserved hosts as their own domain groups. A regex that excludes specific words is awkward, so many apps keep a list of reserved subdomains and reject them in middleware or in the lookup that loads the franchise.
saying these in an interview costs you the question
- Domain parameters are passed after the path parameters
- route() can build a franchise URL without the franchise value
- In Laravel 13 registration order decides between domain and plain routes
- where() constraints only apply to path parameters, never to the host
- An inner domain() group appends to the outer group's host