Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Building Web Apps with Laravel  Module 3 – Requests, routing and middleware

Routing, route parameters and URL generation

Match Laravel 13 routes with parameters, constraints and groups, build URLs from route names, sign a link so edits are rejected, and cache routes.

  • Beginner
  • 15 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

What you will learn

  • Define routes for the common HTTP verbs with required, optional and constrained parameters
  • Predict which route answers a path, and whether it answers 200, 404 or 405
  • Group routes by prefix, name, controller and domain and read the result with route:list
  • Generate URLs from route names, including query values and URL defaults
  • Create and test a signed URL, and cache routes at a deploy while avoiding what makes caching fail

Before you start

What you’ll learn · Before you start

What you will learn

  • Define routes for the common HTTP verbs with required, optional and constrained parameters
  • Predict which route answers a path, and whether it answers 200, 404 or 405
  • Group routes by prefix, name, controller and domain and read the result with route:list
  • Generate URLs from route names, including query values and URL defaults
  • Create and test a signed URL, and cache routes at a deploy while avoiding what makes caching fail

Before you start

On this page

Pathshala’s batch page lives at /batches/7. Ask for /batches/seven and the router answers 405 Method Not Allowed, not the 404 that most developers expect for a URL no page owns. That one surprise comes from three rules: routes are tried in the order they were declared, a constraint belongs to the single route that carries it, and a path that matches under another verb gets a different answer from a path that matches nothing.

This lesson learns those rules by running them. You will resolve nine paths against real routes, group routes by prefix and name, build URLs from route names instead of typing them, sign a link so that an edited copy is refused, and cache routes the way a deploy does, including the mistake that makes the cache fail.

A route pairs a verb and a path pattern with an action

A route: A line in Laravel that maps an HTTP method and a URL path to the code that answers it, such as Route::get('/notices', [NoticeController::class, 'index']). More in “Your first page: route, controller and view” is a rule of the form: for this HTTP verb and this path pattern, run this action. The Route facade has one method for each common verb, get, post, put, patch, delete and options, plus match for a list of verbs and any for all of them. A route registered with get also answers HEAD, which is why php artisan route:list prints GET|HEAD beside it. Here are the routes of this lesson’s probe project:

routes/web.php of the probe project PHP · routes-probe/routes/web.php
<?php

use App\Enums\FeeStatus;
use Illuminate\Support\Facades\Route;

// First match wins, so the fixed path /batches/new sits above the pattern /batches/{batch}.
Route::get('/batches', fn () => 'batch list')->name('batches.index');
Route::get('/batches/new', fn () => 'new batch form')->name('batches.create');
Route::get('/batches/{batch}', fn (string $batch) => "batch {$batch}")
    ->whereNumber('batch')
    ->name('batches.show');

// {roll?} is optional; where() limits it to two capital letters followed by three digits.
Route::get('/batches/{batch}/students/{roll?}', fn (string $batch, ?string $roll = null) => "batch {$batch}, roll ".($roll ?? 'none'))
    ->whereNumber('batch')
    ->where('roll', '[A-Z]{2}[0-9]{3}')
    ->name('batches.students');

// The FeeStatus type hint is an implicit enum binding: only due, paid and late reach the closure.
Route::get('/fees/{status}', fn (FeeStatus $status) => "fees that are {$status->value}")
    ->name('fees.by-status');

Route::post('/batches', fn () => 'create a batch')->name('batches.store');
Route::delete('/batches/{batch}', fn (string $batch) => "remove batch {$batch}")->name('batches.destroy');

The rest of the project is ordinary. Run route:list to see how Laravel reads those lines:

php artisan route:list --except-vendor Python · verbs_session.py
from session import run

# Every route of the probe project: verbs, paths, names and actions.
run(
    """
php artisan route:list --except-vendor
""",
    app=True,
    overlay=("routes-probe",),
)

Output

$ php artisan route:list --except-vendor
  GET|HEAD   batches ........................ batches.index › routes/web.php:7
  POST       batches ....................... batches.store › routes/web.php:23
  GET|HEAD   batches/new ................... batches.create › routes/web.php:8
  GET|HEAD   batches/{batch} ................. batches.show › routes/web.php:9
  DELETE     batches/{batch} ............. batches.destroy › routes/web.php:24
  GET|HEAD   batches/{batch}/students/{roll?} batches.students › routes/web.p…
  GET|HEAD   fees/{status} ................ fees.by-status › routes/web.php:20

                                                            Showing [7] routes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 verbs_session.py

Each row is one verb, one path pattern, a name and where the action was defined. --except-vendor hides the routes that packages register, so only yours are shown. The pattern batches/{batch} appears twice, once for GET and once for DELETE, and that is not a mistake: the router distinguishes routes by verb and pattern together.

HTML forms can send only GET and POST. To reach a PUT, PATCH or DELETE route from a form, the Blade directive @method('PUT') adds a hidden _method field, and the HTTP kernel treats the request as the verb named there before it chooses a route. A script or a mobile app does not need the field, because it can send the real verb.

Page-based routing exists too

The Folio package routes by files: a Blade file under resources/views/pages becomes a URL. It suits content-heavy sites. This track uses route files, which keep every URL in one searchable place.

Parameters and constraints decide what matches, and the first match wins

A route parameter: A part of a route's path pattern in curly braces, such as {batch}. It captures that part of the URL and hands it to the action by name. is a part of the path pattern in curly braces. It captures that segment of the URL and passes it to the action by name. A parameter followed by a question mark, as in {roll?}, is optional, and the action must give it a default value. A route constraint: A rule that limits what a route parameter may capture, such as whereNumber or a regular expression. A route whose parameter fails the rule is skipped. narrows what a parameter may capture: whereNumber('batch') accepts digits only, where('roll', '[A-Z]{2}[0-9]{3}') takes any regular expression, and whereAlpha, whereUuid and whereIn cover common shapes. A type-hinted backed enum works as a built-in constraint, called implicit enum binding: A route parameter type-hinted with a backed enum matches only the enum's values; any other value answers 404 and the action does not run.. Run nine paths through the real kernel:

Nine paths against the routes of the probe project Python · resolve_session.py
from session import run

# Resolve a list of paths against constrained, optional and enum routes.
run(
    """
php resolve_paths.php /batches /batches/new /batches/7 /batches/seven /batches/7/students /batches/7/students/AB123 /batches/7/students/ab123 /fees/paid /fees/refunded
""",
    app=True,
    overlay=("routes-probe",),
)

Output

$ php resolve_paths.php /batches /batches/new /batches/7 /batches/seven /batches/7/students /batches/7/students/AB123 /batches/7/students/ab123 /fees/paid /fees/refunded
GET /batches                    200  batch list
GET /batches/new                200  new batch form
GET /batches/7                  200  batch 7
GET /batches/seven              405  Allow: DELETE
GET /batches/7/students         200  batch 7, roll none
GET /batches/7/students/AB123   200  batch 7, roll AB123
GET /batches/7/students/ab123   404
GET /fees/paid                  200  fees that are paid
GET /fees/refunded              404

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 resolve_session.py

Read the results one at a time. /batches/new matched the fixed route and /batches/7 the constrained one. /batches/7/students has no roll, so the optional parameter took its default. AB123 matched the roll pattern and ab123 did not, so it answered 404. /fees/refunded answered 404 too, because the FeeStatus enum has no such case, and no code of yours ran.

The odd one is /batches/seven, which answered 405 with Allow: DELETE. The GET route rejected seven because of its whereNumber, but the DELETE /batches/{batch} route carries no constraint, so its pattern accepts any word. The router then found a path that exists under another verb. Step through the order:

Interactive · stage by stage Trying the routes for one request Step through the routes in the order the router tries them for one request.
Why GET /batches/seven answered 405, step 6 of 6. Showing GET /batches/seven, GET /batches, GET /batches/new, GET /batches/{batch}, DELETE /batches/{batch} (pattern matches), 405 Method Not Allowed. In focus: 405 Method Not Allowed.GET /batches/seventhe requestGET /batchespath is shorterGET /batches/newfixed textGET /batches/{batch}whereNumber rejects itDELETE /batches/{batch}pattern matches405 Method Not AllowedAllow: DELETE

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The request arrives. The router holds the routes in the order they were declared and asks each one, from the top, whether it matches the method and the path of the request.
  2. A shorter path. GET /batches has one segment less, so its pattern cannot match a path with two segments.
  3. A fixed word. GET /batches/new matches only the text new in its second segment. The request has seven there, so it does not match.
  4. The constraint says no. GET /batches/{batch} has the right shape, but whereNumber requires digits and seven is not digits. The route is skipped.
  5. A sibling that matches. DELETE /batches/{batch} has the same path pattern and no constraint, so seven fits the pattern. Its verb is DELETE, which is not GET.
  6. Wrong verb, not wrong path. A path that matches only under another verb is answered with 405 and an Allow header that lists the verbs that would work. A 404 would mean no pattern matched at all.

A 405 therefore means the path pattern exists, only under other verbs, and a 404 means that no pattern matched at all. A constraint protects only the route that carries it. When one pattern appears in several routes, put the constraint where all of them share it: in a group, or with Route::pattern('batch', '[0-9]+') in a service provider’s boot method, which applies one rule to every parameter of that name.

The same rule explains why order matters. The router asks the routes from the top and the first match wins:

A pattern above a fixed path

If Route::get('/batches/{batch}', ...) is declared above Route::get('/batches/new', ...) and has no constraint, the word new is captured as a batch and the form never shows. In a test of that order, /batches/new answered 200 with the body batch new. Declare the fixed path first, or add whereNumber, which is what the probe project does.

Checkpoint

Check: Parameters, constraints and first match

  1. Question 1 of 2 GET /batches/{batch} has whereNumber('batch'), and DELETE /batches/{batch} has no constraint. What does GET /batches/seven answer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 405 with Allow set to DELETE

    The router skipped the GET route because of its constraint, then found the DELETE route with the same pattern and no constraint. A path that exists under another verb is answered with 405 and an Allow header.

  2. Question 2 of 2 GET /batches/{batch} with no constraint is declared above GET /batches/new. Which route answers /batches/new?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The /batches/{batch} route, with batch set to new

    The router tries the routes from the top and stops at the first match. Declare fixed paths before patterns, or constrain the pattern so it cannot match the fixed word.

Groups share a prefix, a name, a controller and middleware

Routes for one feature repeat the same settings. A route group: A way to give several routes the same prefix, name prefix, controller, middleware or domain once, instead of repeating it on each route. states them once. prefix() adds a path prefix, name() adds a name prefix, controller() sets the class whose methods the routes name, middleware() attaches middleware, and domain() limits the group to a host. The settings combine, and the group’s closure holds the routes:

routes/web.php with two groups PHP · groups-probe/routes/web.php
<?php

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

// One group sets the controller, the path prefix, the name prefix and a middleware alias for all its routes.
Route::controller(FeeController::class)
    ->prefix('fees')
    ->name('fees.')
    ->middleware('throttle:60,1')
    ->group(function () {
        Route::get('/', 'index')->name('index');
        Route::get('/{receipt}', 'show')->whereNumber('receipt')->name('show');
    });

// A domain group: the first label of the host becomes the {centre} parameter.
Route::domain('{centre}.pathshala.test')
    ->name('portal.')
    ->group(function () {
        Route::get('/', fn (string $centre) => "portal of {$centre}")->name('home');
    });

Ask Laravel what those few lines produced:

php artisan route:list --except-vendor -v Python · groups_session.py
from session import run

# php artisan route:list -v shows the full path, the full name, the action and the middleware of every route.
run(
    """
php artisan route:list --except-vendor -v
""",
    app=True,
    overlay=("groups-probe",),
)

Output

$ php artisan route:list --except-vendor -v
  GET|HEAD       {centre}.pathshala.test/ .... portal.home › routes/web.php:20
                 ⇂ web
  GET|HEAD       fees ....................... fees.index › FeeController@index
                 ⇂ web
                 ⇂ throttle:60,1
  GET|HEAD       fees/{receipt} ............... fees.show › FeeController@show
                 ⇂ web
                 ⇂ throttle:60,1

                                                            Showing [3] routes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 groups_session.py

The first group made two routes whose paths are fees and fees/{receipt}, whose names are fees.index and fees.show, and whose action is FeeController@index and @show. The -v flag adds the middleware under each route: web, which every route in routes/web.php receives, and throttle:60,1, the group’s own. The domain group made a route on {centre}.pathshala.test, and the first label of the host reaches the action as the $centre parameter, so one definition can serve a separate subdomain for every centre.

Groups nest. A prefix inside a prefix concatenates, and so do name prefixes, which is why the trailing dot in name('fees.') matters: without it the names would read feesindex. When a route behaves oddly, route:list -v is the first place to look, because it prints the final path, the final name and the final middleware, after every group has had its say.

Names turn into URLs, and defaults fill the gaps

Typing /batches/7 into a view works until the path changes. A named route: A route given a label with ->name('notices.index'), so its URL can be built anywhere with route('notices.index') instead of a hardcoded path. More in “Your first page: route, controller and view” lets a view write route('batches.show', $batch) and leaves the path to the router. Passing a model works because the router asks the model for its route key, its id unless you change it. Values that match no parameter become the query string. Run the calls:

Building URLs from route names Python · url_session.py
from session import run

# Names and values in, paths out; then the same call with URL::defaults() set.
run(
    """
php url_generation.php
""",
    app=True,
    overlay=("urls-probe",),
)

Output

$ php url_generation.php
route('batches.show', 7)             http://localhost:8000/batches/7
route('batches.show', $batch)        http://localhost:8000/batches/7
route('batches.show', $batch, false) /batches/7
route('batches.index', [...])        http://localhost:8000/batches?page=2&q=evening%20batch
url('/notices')                      http://localhost:8000/notices
route('timetable')                   UrlGenerationException: Missing required parameter for [Route: timetable] [URI: {centre}/timetable] [Missing parameter: centre].
route('timetable') with a default    http://localhost:8000/north/timetable
route('timetable', 'south')          http://localhost:8000/south/timetable

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 url_session.py

The first two lines build the same URL, one from the number 7 and one from a model with that id. The third passes false and gets the path only, which is what a link inside a page often wants. The batches.index call shows the leftovers: page and q follow a question mark, and the space in evening batch is written %20. The URL begins with the application’s root, which comes from the APP_URL setting when there is no request to take it from.

Then route('timetable') fails. Its pattern is {centre}/timetable, and nothing says which centre. The error names the route and the missing parameter, and that message is the reason to prefer names over strings: a typo is found when the call runs, not when a visitor clicks. A URL defaults: Values set with URL::defaults that fill route parameters the caller of route() leaves out, such as the centre in {centre}/timetable. repairs it. After URL::defaults(['centre' => 'north']) the same call returns /north/timetable, and an explicit value, as in route('timetable', 'south'), still wins. The documentation suggests calling it from a route middleware, which can read the signed-in user or the host, and adds a rule: give that middleware a higher priority than SubstituteBindings, or implicit model binding may run before the default exists.

Interactive · stage by stage From a name to a URL Step through one call to route(), from the name to the finished URL.
How a route name becomes a URL, step 6 of 6. Showing route() call, Find the route, Fill the parameters (south), Add the leftovers, Add the root, The URL. In focus: The URL.route() callnameFind the routeby its nameFill the parameterssouthAdd the leftoversas a query stringAdd the rootor leave it offThe URL/south/timetable

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A name and a value. The call route('timetable', 'south') gives a route name and one value. Nothing is requested; the call only builds text.
  2. The route is found by name. The router looks the name up in its list. A name that no route has is an error, which is why a typo shows up at once and not as a dead link.
  3. Parameters are filled in. The path is {centre}/timetable, so south takes the place of {centre}. A parameter that you leave out is taken from URL::defaults, and an error is raised if neither supplies it.
  4. Leftovers become the query. A value that matches no parameter is added after a question mark, so route('batches.index', ['page' => 2]) ends with ?page=2, and spaces are written as %20.
  5. The root is added. By default the URL is absolute and starts with the application's root. Passing false as the third argument leaves the root off and gives the path only.
  6. The finished URL. The result for route('timetable', 'south') is the root followed by /south/timetable. If the path changes later, the call stays the same.

Key idea

A route name is the stable part of a link and the path is the changeable part. Build every URL from the name and the values, and a change to a path touches one line.

Checkpoint

Check: Groups share prefix, name, controller and middleware · Building URLs from names, with defaults

  1. Question 1 of 2 A group has prefix('fees') and name('fees.'). Inside it, Route::get('/{receipt}', ...)->name('show'). What are the final path and name?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: fees/{receipt} and fees.show

    A group's prefix is added to the path and its name prefix to the name. The trailing dot of 'fees.' is part of the prefix, so the joined name is fees.show.

  2. Question 2 of 2 The route timetable has the path {centre}/timetable. What does route('timetable') do after URL::defaults(['centre' => 'north'])?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It returns the URL ending in /north/timetable

    URL::defaults supplies a value for a parameter the caller leaves out. Without it the call throws a UrlGenerationException that names the missing parameter.

Some links must work without a login: a receipt a parent opens from an email, or a download from a message. If the link is /receipts/1042, anyone can try /receipts/1043. A signed URL: A URL with a signature in its query, a keyed hash of the whole URL. The server refuses a copy that was edited or has passed its expiry. adds a signature to the query, a keyed hash of the whole URL made with the application key. The signed middleware, an alias you can put on any route, makes the hash again from what arrived and refuses the request with 403 when the two differ. URL::temporarySignedRoute() also adds an expiry time, and the signature covers that too. Make a link, then abuse it:

A temporary signed link, used three ways Python · signed_session.py
from session import run

# A temporary signed URL, requested untouched, edited and expired.
run(
    """
php signed_links.php
""",
    app=True,
    overlay=("signed-probe",),
)

Output

$ php signed_links.php
link  /receipts/1042  query values: expires, signature

as made                           200
fee changed from 1042 to 1043     403
signature removed                 403
31 minutes later                  403

permanent link  /receipts/1042  query values: signature
permanent link as made            200

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 signed_session.py

The link as made answered 200. Changing the fee number answered 403, and so did removing the signature. A request made 31 minutes later, with the clock moved by Carbon::setTestNow, answered 403 for a link that was valid for 30. The permanent link, made with signedRoute, has only a signature.

Interactive · message by message Make, use, edit, expire Step through one receipt link that is made, used, edited and used too late.
A signed link, edited and expired, step 4 of 4. Messages so far: App to Browser: link + sig; Browser to Middleware: GET 1042; Browser to Middleware: GET 1043; Browser to Middleware: GET 1042 late.Appmakes linkBrowseropens itMiddlewarelate, 403link + sigGET 1042GET 1043GET 1042 late

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. The app makes the link. temporarySignedRoute adds an expiry time to the URL and then adds a signature, a keyed hash of the whole URL made with the application key. Only a holder of the key can make a signature that fits.
  2. The link is used as made. The browser asks for exactly the URL it was given. The middleware makes the hash again from the URL it received, compares it with the one in the URL, and checks the expiry time. Both pass.
  3. One digit is changed. A curious visitor changes 1042 to 1043. The hash made from the new URL no longer equals the signature in the query, so the middleware stops the request with 403.
  4. The link is used too late. The link is untouched, but the time is 31 minutes after it was made and the expiry was 30 minutes. The signature is valid and the middleware still answers 403, because the expiry time has passed.

The signature proves that nobody changed the link. It does not prove who holds it. A forwarded email, a browser history or a log line gives the same working link to a stranger, until the expiry passes. For a private document use a short expiry, and put the auth middleware on the route as well when the link must work only for the signed-in owner. Laravel’s own email verification links are signed URLs in the same way.

Checkpoint

Check: Signed URLs

  1. Question 1 of 2 A parent edits the fee number in a signed receipt link from 1042 to 1043 and keeps the rest. What does the signed middleware answer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 403, because the signature no longer fits the URL

    The middleware makes the hash again from the URL it received and compares it with the signature in the query. Any change to the path or to a query value breaks the match, and the answer is 403.

  2. Question 2 of 2 Pathshala emails each parent a signed link to a fee receipt with a 30-minute expiry. A forwarded email reaches a stranger within the half hour. What does the signature protect against?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Editing the link, but not use of the intact link by whoever holds it

    A signed URL says that the link was not changed and, with an expiry, that it is not too old. It does not say who is using it. When the receipt is private the route also needs the auth middleware, or a very short expiry.

Route caching makes startup faster and rejects clashing names

On every request without a cache, Laravel loads the route files, runs the code in them and builds the route list. php artisan route:cache does that once and writes the result to bootstrap/cache/routes-v7.php. Requests then load one file. You run the command at each deploy, after the code is in place. Run it on the probe project, look at the list and clear it again:

Cache the routes, list them, clear the cache Python · cache_session.py
from session import run

# Cache the routes of the probe project, list them from the cache, clear the cache again.
run(
    """
php artisan route:cache
ls bootstrap/cache
php artisan route:list --except-vendor --path=batches
php artisan route:clear
ls bootstrap/cache
""",
    app=True,
    overlay=("routes-probe",),
)

Output

$ php artisan route:cache
   INFO  Routes cached successfully.
$ ls bootstrap/cache
packages.php
routes-v7.php
services.php
$ php artisan route:list --except-vendor --path=batches
  GET|HEAD   batches ........................................... batches.index
  POST       batches ........................................... batches.store
  GET|HEAD   batches/new ...................................... batches.create
  GET|HEAD   batches/{batch} .................................... batches.show
  DELETE     batches/{batch} ................................. batches.destroy
  GET|HEAD   batches/{batch}/students/{roll?} ............... batches.students

                                                            Showing [6] routes
$ php artisan route:clear
   INFO  Route cache cleared successfully.
$ ls bootstrap/cache
packages.php
services.php

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cache_session.py

The list after caching has the same six batch routes, now read from the cache file. The cache is a snapshot. A route you add later is invisible until you run route:cache again, and while a cache exists, an edit to routes/web.php does nothing. Clear the cache with route:clear on a development machine, and cache only in production.

The other thing caching does is check. Two routes may share a name while the app runs, and a name lookup then picks one of them. The cache cannot store two routes under one name:

Two routes named batches.index Python · cache_fail_session.py
from session import run

# Two routes with one name: it works while the app runs, but route:cache refuses.
run(
    """
php artisan route:list --except-vendor
!php artisan route:cache
""",
    app=True,
    overlay=("dup-probe",),
)

Output

$ php artisan route:list --except-vendor
  GET|HEAD       batches .................... batches.index › routes/web.php:5
  GET|HEAD       batches/archive ............ batches.index › routes/web.php:8

                                                            Showing [2] routes
$ php artisan route:cache
   LogicException

  Unable to prepare route [batches/archive] for serialization. Another route has already been assigned name [batches.index].

  at vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php:257
    253▕             $route->name($this->generateRouteName());
    254▕
    255▕             $this->add($route);
    256▕         } elseif (! is_null($symfonyRoutes->get($name))) {
  ➜ 257▕             throw new LogicException("Unable to prepare route [{$route->uri}] for serialization. Another route has already been assigned name [{$name}].");
    258▕         }
    259▕
    260▕         $symfonyRoutes->add($route->getName(), $route->toSymfonyRoute());
    261▕

      +19 vendor frames

  20  artisan:16
      Illuminate\Foundation\Application::handleCommand(Object(Symfony\Component\Console\Input\ArgvInput))
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cache_fail_session.py

The list shows both routes, and route:cache stops with a LogicException that names the second route. The probe’s routes are closures, and they cached without trouble in Laravel 13.34. Even so, a route that points at a controller method is the safer form to deploy, because the cache then stores a class and a method name and not a copy of code.

Interactive · step by step What the cache changes Step through a deploy that caches the routes, and the check that stops it when two routes share a name.
What route:cache changes at a deploy, step 5 of 5. Showing Route files, route:cache, routes-v7.php, A request. In focus: A request.read oncewritesloadsRoute filesroute:cacheroutes-v7.phpA request

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Routes live in files. Without a cache, every request loads routes/web.php and the other route files, runs their code and builds the route list again.
  2. The command reads them once. php artisan route:cache loads every route file, checks the names, and prepares the routes for storing.
  3. A clash stops it. Two routes named batches.index make the command stop with a LogicException that names the second route, so the clash is found at deploy time and not on a visitor's request.
  4. One file is written. With unique names the command writes a single file, bootstrap/cache/routes-v7.php, that holds all the routes in a ready form.
  5. Requests load the cache. Each request now loads that one file and does not run the route files. A route you add afterwards is not seen until you run route:cache again, or route:clear to go back to the files.

Checkpoint

Check: Route caching

  1. One question Two routes in routes/web.php are both named batches.index. The app works in development. What happens at the deploy step php artisan route:cache?

    Choose one answer.

    Show the answer: One question

    Answer: The command stops with a LogicException that names the second route

    Route caching checks for clashing names, so the mistake that the running app hid is found at deploy time.

Practise

Rebuild the rules on your own Pathshala project, and check each with a request:

  1. Declare GET /batches/{batch} with whereNumber('batch') and DELETE /batches/{batch} without one. Request /batches/seven, note the status, then move the rule into a Route::pattern call and request it again.
  2. Put three routes into a group with a prefix, a name prefix and the auth alias. Read the result with php artisan route:list -v, and write down the final path, name and middleware of each.
  3. Make a receipts.show route with the signed middleware, build a temporary signed URL in a feature test, and assert 200 for the link, 403 for an edited copy and 403 after $this->travel(31)->minutes().

Then work through the self-check.

Practice that adapts to you

Practise this lesson

17 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 route:list shows GET|HEAD for a route made with Route::get. Why?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: A route registered for GET also answers HEAD requests

    A GET route also answers HEAD, so route:list prints both. A POST route prints POST alone.

  2. Question 2 of 17 An HTML form must reach Route::delete('/batches/{batch}'). What does the form need?

    Choose one answer.

    Show the answer: Question 2 of 17

    Answer: Method POST and a hidden _method field with the value DELETE, from @method('DELETE')

    The method override is a hidden field called _method. The @method directive writes it, and the HTTP kernel enables the override when it starts to handle the request.

  3. Question 3 of 17 Which is the better design for marking a fee instalment as paid, assuming the call changes data?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: A POST or PATCH route, because a GET must be safe to repeat and to prefetch

    A GET must not change data. Marking a payment is a change, so it uses POST or PATCH from a form or a script.

  4. Question 4 of 17 The pattern is /batches/{batch}/students/{roll?}. What must the action's roll parameter have?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: A default value, such as ?string $roll = null

    An optional parameter may be absent from the URL, so the action must supply a default, as the probe's closure does with $roll = null.

  5. Question 5 of 17 A route takes (FeeStatus $status) and the enum has the cases due, paid and late. What does GET /fees/refunded answer?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: 404, and the action does not run

    A type-hinted backed enum limits the parameter to its cases. Any other value is a 404 and the action never runs.

  6. Question 6 of 17 /batches/new shows a batch called new. Which two changes each fix it?

    Choose one answer.

    Show the answer: Question 6 of 17

    Answer: Declare /batches/new first, or add whereNumber to /batches/{batch}

    The first matching route wins. Put the fixed path before the pattern, or constrain the pattern so that it cannot take the fixed word.

  7. Question 7 of 17 Five routes use {batch}, and every one must accept digits only. Where is the best place for the rule?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: One Route::pattern('batch', '[0-9]+') call in the boot method of a service provider

    A global pattern is stated once and applies to every parameter of that name, so no sibling route can be left unconstrained.

  8. Question 8 of 17 How do you see which middleware a grouped route finally has?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: php artisan route:list -v

    route:list -v lists the final middleware of every route. It includes the web group and the group's own.

  9. Question 9 of 17 A domain group uses {centre}.pathshala.test. How does the action know the centre?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: The first label of the host arrives as the $centre parameter

    Domain parameters are captured like path parameters, so one group can serve a separate subdomain for each centre.

  10. Question 10 of 17 A teammate writes name('fees') with no trailing dot on a group and Route::get(..)->name('index') inside it. What is the route's name?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: feesindex

    The group's name prefix is put before the route's name as written. The trailing dot has to be in the prefix.

  11. Question 11 of 17 What does route('batches.show', $batch, false) return in the lesson's example?

    Choose one answer.

    Show the answer: Question 11 of 17

    Answer: /batches/7, the path without the root

    With absolute set to false the URL has no root, so a link inside the page can be written as a path.

  12. Question 12 of 17 What does route('batches.index', ['page' => 2, 'q' => 'evening batch']) return for a route with no parameters?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: The path followed by ?page=2&q=evening%20batch

    Extra values go after a question mark. A space is written as %20, as the lesson's output shows.

  13. Question 13 of 17 A middleware sets URL::defaults from the signed-in user's centre, and implicit model binding on the same routes starts to misbehave. What does the documentation say to do?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: Give that middleware a higher priority than SubstituteBindings

    URL defaults can interfere with implicit bindings, so the middleware that sets them must run before SubstituteBindings, which the priority method arranges.

  14. Question 14 of 17 What makes a temporarySignedRoute link stop working after 30 minutes?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: An expires value in the query that the signature covers

    The expiry time is part of the URL and part of what the signature covers, so it cannot be changed without breaking the signature.

  15. Question 15 of 17 Which link needs the signed middleware most?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: An unsubscribe link in an email that opens without a login

    Signed URLs are for routes that must work without a login but should not be guessable or editable, such as an emailed receipt or unsubscribe link.

  16. Question 16 of 17 After route:cache, you add a route to routes/web.php. Why does the new URL answer 404?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: Requests load the cache file, and it was made before the route existed

    A route cache is a snapshot of the route files. After an edit, run route:cache again at the deploy or route:clear while developing.

  17. Question 17 of 17 Where should route:cache run?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: As a step of the deploy, in production only

    The documentation says to run it during deployment. A development machine clears the cache so edits are seen.

Interview questions

Warm-up: what is the difference between a 404 and a 405 from the router?

A 404 means that no route matched the path at all, so the router has nothing that could answer it. A 405 means that at least one route has a matching path pattern, but none of the matching routes accept the verb of this request, and the response carries an Allow header listing the verbs that would work. A common cause of a surprising 405 is a constraint that sits on one route and not on its sibling: the GET route refuses a value and a DELETE route with the same pattern and no constraint accepts it. The fix is to move the constraint to where every sibling shares it, in a group or in a global Route::pattern, and to test the failing path with a real request.

Three longer questions grow out of these rules:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    After a refactor the "new batch" form at /batches/new shows a page titled "Batch new", and no error is logged. Explain how a routing change can cause this, how you would confirm it, and how you would fix it so it cannot come back.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A deploy script runs php artisan route:cache and the release stops with a LogicException about a route name, although the site worked in staging. Find the cause, fix it, and describe what you would add to the pipeline.

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. The router has /batches/{batch} and /batches/new. Why does the order of the two matter?

    Matching follows declaration order, and the earliest route that fits answers. Declared first, /batches/{batch} would read new as a batch id, so the fixed path goes above the pattern.

  2. What is the difference between a 404 and a 405 from the router?

    A 404 means no route's path matched. A 405 means a path matched but no matching route accepts this verb; the response's Allow header lists the verbs that would work.

  3. How can a where() constraint on one route turn a bad id into a 405 instead of a 404?

    A constraint belongs only to the route that carries it. If a sibling route with the same pattern and another verb has no constraint, it still matches the value, so the router answers 405 instead of 404. Put the constraint where every sibling shares it.

  4. What can a route group set once for all the routes inside it?

    A path prefix, a name prefix, a controller, middleware or a domain. php artisan route:list -v shows the final path, name and middleware each route ends up with.

  5. Why build links with route() and a route name instead of writing the path?

    The name is the stable part and the path can change: a link from route() follows the route, an unknown name or a missing parameter fails loudly, and URL::defaults can fill a parameter every route shares.

  6. What does a signed URL prove, and what does it not?

    It proves the link was not edited and has not expired: Laravel answers 403 to a changed or expired one. It does not prove who is using it, so it is no replacement for a login check.

  7. What does php artisan route:cache do, and when does it fail?

    Run at each deploy, it builds the route list once and writes it to one file in bootstrap/cache, so requests load that file instead of the route files. It is a snapshot (a new route needs route:cache again), and it stops with an exception when two routes share a name.

Key takeaways

  • A route pairs a verb and a path pattern with an action, the router tries them in the order they were declared, and the first match wins, so a fixed path such as /batches/new must come before /batches/{batch}.
  • A constraint belongs to the route that carries it, so a sibling route with the same pattern can accept a value the first refused, and the result is a 405 with an Allow header, not a 404.
  • A route group sets a prefix, a name prefix, a controller, middleware or a domain once, and php artisan route:list -v shows the final path, name and middleware of every route.
  • Building URLs from route names with route() keeps links correct when a path changes, turns unknown names and missing parameters into errors, and lets URL::defaults fill a parameter that every route shares.
  • A signed URL is refused with 403 when it is edited or past its expiry, but it proves integrity and not identity, and route:cache writes one cache file at a deploy and fails when two routes share a name.

Terms in this lesson

implicit enum binding
A route parameter type-hinted with a backed enum matches only the enum's values; any other value answers 404 and the action does not run.
route constraint
A rule that limits what a route parameter may capture, such as whereNumber or a regular expression. A route whose parameter fails the rule is skipped.
route group
A way to give several routes the same prefix, name prefix, controller, middleware or domain once, instead of repeating it on each route.
route parameter
A part of a route's path pattern in curly braces, such as {batch}. It captures that part of the URL and hands it to the action by name.
signed URL
A URL with a signature in its query, a keyed hash of the whole URL. The server refuses a copy that was edited or has passed its expiry.
URL defaults
Values set with URL::defaults that fill route parameters the caller of route() leaves out, such as the centre in {centre}/timetable.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.