skip to content

In Laravel, what does Route::pattern() do, where should you call it, and how does it interact with a route's own where()?

level: middleimportance: should knowfreq 30%

answer

  1. one regex per parameter name, app-wide
  2. AppServiceProvider boot method
  3. merged in when each route is created
  4. group and route where() win
  5. Route::patterns for several names

basics

~10 s

Route::pattern('id', '[0-9]+') applies that regex to every route parameter named id; call it in AppServiceProvider::boot so it exists before routes load, and a where() on a group or a single route overrides it.

solid answer

~40 s

`Route::pattern()` registers a **global constraint** keyed by parameter name, and `Route::patterns([...])` registers several. The router does not check it at request time; it merges the global patterns into each route **as the route is created**, so the pattern must exist before the route files load. That is why the docs put it in `AppServiceProvider::boot()`. Precedence follows the merge order: global patterns first, then a group's `where`, then anything chained on the route, such as `->whereUlid('id')`, which wins. The classic trap is a broad name: `Route::pattern('id', '[0-9]+')` also constrains a ULID-based `/invoices/{id}` route, which then 404s until that route overrides the pattern or uses a different parameter name.

code

php · 15 lines
php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Route::pattern('year', '[0-9]{4}');
        Route::pattern('month', '0[1-9]|1[0-2]');
    }
}

go deeper

for a junior

Know that Route::pattern constrains every parameter with a given name and that it belongs in AppServiceProvider::boot.

for a middle

Explain that patterns are merged in when routes are created, and the precedence of global, group and route-level constraints.

for a senior

Spot the broad-name trap when new resources reuse a globally constrained name, and fix it locally with an override or a clearer parameter name.

for a principal

Decide which parameter names carry app-wide meaning and document them, so global constraints stay predictable as the app grows.

## What a global pattern is Writing `->whereNumber('year')` on every archive route gets repetitive. **`Route::pattern($key, $regex)`** stores a regex for a parameter **name**, and every route that uses that name receives the constraint automatically. **`Route::patterns([...])`** calls `pattern()` for each entry: ```php use Illuminate\Support\Facades\Route; public function boot(): void { Route::patterns([ 'year' => '[0-9]{4}', 'month' => '0[1-9]|1[0-2]', ]); } ``` After this, `/news/{year}` and `/news/{year}/{month}` are constrained without any `where()` calls. ## When the pattern takes effect This detail explains most surprises. The router keeps global patterns in an array and applies them in `addWhereClausesToRoute()`, which runs inside route creation: 1. A route is created from `Route::get(...)` or another verb method. 2. Group attributes, including a group-level `where`, are merged into it. 3. The router merges the **current** global patterns with the route's `where` attributes and applies them. 4. Anything chained afterwards (`->where()`, `->whereNumber()`) is applied last. Consequences: - A pattern registered **after** a route was created never reaches that route. - `AppServiceProvider::boot()` is the recommended place because application service providers boot before the framework loads the route files. - Calling `Route::pattern()` halfway through `routes/web.php` affects only the routes defined below it, which is legal but confusing. ## Precedence | Level | How it is set | Wins over | |---|---|---| | Global | `Route::pattern()` / `Route::patterns()` | nothing | | Group | `Route::where([...])->group(...)` or `Route::whereNumber('id')->group(...)` | global | | Route | `->where()` or a `where*` helper chained on the route | global and group | So a route can always opt out of a global rule by constraining the same parameter itself. ## The broad-name trap Global patterns key on the **name only**, not on the model or the route. In a news site: - `Route::pattern('id', '[0-9]+')` suits articles with integer IDs. - Later someone adds `/invoices/{id}` for invoices keyed by ULIDs. - Every invoice URL now 404s, because the ULID contains letters and fails `[0-9]+`. Two fixes, both local to the new route: 1. Override: `->whereUlid('id')` on the invoice route. 2. Rename: use `{invoice}` instead of `{id}`, which also reads better and suits route model binding. The longer-term lesson is to reserve global patterns for names that mean the same thing everywhere, such as `year`, `month` or `locale`, and avoid them for generic names like `id` in apps with mixed key types. ## Group constraints as the middle ground Between one route and the whole app sits the group. A group registrar accepts `where()` and every `where*` helper: ```php Route::whereNumber('year')->prefix('news/{year}')->group(function () { Route::get('/', [ArchiveController::class, 'year']); Route::get('/{month}', [ArchiveController::class, 'month']); }); ``` The group's `where` is merged into each route it contains, overrides any global pattern of the same name, and stops at the group's edge. For a rule that belongs to one feature, such as the news archive, this is usually a better fit than a global pattern that every future route inherits. ## Debugging a pattern - `php artisan route:list` does not show constraints, so a globally constrained route looks unconstrained there. - A quick check is a feature test that requests the URL and asserts the status, or a look at `Route::getRoutes()` in Tinker, where each route's `wheres` array lists the final regexes. - Search the providers for `Route::pattern` whenever a URL 404s for no visible reason. ## Pitfalls interviewers probe - **Believing global patterns override a route's `where()`.** The route's own constraint wins. - **Registering patterns in a route file after the routes they target.** They arrive too late. - **Expecting patterns to validate query-string input.** They only constrain path parameters. - **Using a generic name** such as `id` or `slug` across resources with different formats. - **Forgetting that patterns are invisible at the call site.** A reader of `routes/web.php` sees an unconstrained `{year}`; a comment or a group-level constraint keeps the rule discoverable. In an interview, the strongest answer names all three levels, explains that the merge happens when routes are created, and gives the ULID example as the reason to keep global names narrow.

  • Why does Route::pattern() called at the bottom of routes/web.php not constrain the routes above it?
    The router applies global patterns when each route is created, merging the patterns that exist at that moment. Routes defined earlier in the file were already created without it. Register patterns in `AppServiceProvider::boot()`, which runs before the route files load, so every route sees them.
  • How do you constrain one group of Laravel routes without a global pattern?
    Use a group registrar: `Route::whereNumber('id')->group(function () { ... })` or `Route::where(['id' => '[0-9]+'])->group(...)`. The group's `where` is merged into each route in the group and overrides a global pattern of the same name, while routes outside the group are unaffected.

saying these in an interview costs you the question

  • A global Route::pattern overrides a where() written on the route
  • Global patterns are checked at request time, so they can be registered anywhere
  • Route::pattern also validates query-string parameters
  • route:list shows the constraint each global pattern adds
  • A pattern named id only affects routes in the file that declared it