skip to content

In Laravel, how do where() and helpers such as whereNumber constrain route parameters, and what happens when a URL fails them?

level: middleimportance: must knowfreq 60%

answer

  1. a regex per parameter
  2. whereNumber is [0-9]+
  3. whereIn: fixed values or enum cases
  4. failed constraint: route does not match
  5. routing check, not validation

basics

~20 s

->where('id', '[0-9]+') attaches a regex to one parameter, and whereNumber, whereAlpha, whereAlphaNumeric, whereUuid, whereUlid and whereIn are shortcuts; a URL that fails the regex does not match that route, so it usually ends in 404.

solid answer

~30 s

Constraints are part of route matching. `->where('year', '[0-9]{4}')` or `->where(['year' => '[0-9]{4}', 'slug' => '[a-z0-9-]+'])` attaches regexes, and the helpers expand to fixed ones: `whereNumber` is `[0-9]+`, `whereAlpha` `[a-zA-Z]+`, `whereAlphaNumeric` `[a-zA-Z0-9]+`, plus `whereUuid`, `whereUlid` and `whereIn($param, $values)`, which also accepts enum cases. When a segment fails, that route simply does not match; the router tries the others and, if none fits, returns 404. It is not validation, so there is no 422 and no error message. Watch the helpers' exact regexes: `whereAlpha` rejects hyphenated slugs such as `budget-vote`, and `whereNumber` accepts `0042`.

code

php · 15 lines
php
<?php

// routes/web.php - news archive
use App\Enums\Section;
use App\Http\Controllers\NewsController;
use Illuminate\Support\Facades\Route;

Route::get('/news/{year}', [NewsController::class, 'year'])
    ->where('year', '[0-9]{4}');

Route::get('/news/{slug}', [NewsController::class, 'show'])
    ->where('slug', '[a-z0-9-]+');

Route::get('/sections/{section}', [NewsController::class, 'section'])
    ->whereIn('section', Section::cases());

go deeper

for a junior

Know where() and the helper names, and that a URL failing a constraint does not reach the route.

for a middle

Explain the exact regexes behind the helpers, why a failed constraint ends in 404 rather than 422, and how constraints separate look-alike routes.

for a senior

Diagnose unexplained 404s by reading constraints, avoid helpers whose regex is wrong for the data, and keep constraints as routing guards rather than validation.

for a principal

Standardise URL shapes and identifier formats so constraints stay simple, and decide which identifiers appear in public URLs at all.

## Why constrain a parameter By default a route parameter matches any run of characters except `/`. That is often too loose: - `/news/{year}` would treat `/news/budget-vote` as a year. - An ID route would try to load `/invoices/abc` and fail deep in the code instead of at the router. - Two routes with the same shape, such as `/news/{year}` and `/news/{slug}`, cannot be told apart. A **constraint** is a regular expression the segment must match for the route to match. ## Writing constraints with where() `where()` is a method on the route instance, so it chains after the definition: ```php use Illuminate\Support\Facades\Route; Route::get('/news/{year}/{month}', $action) ->where(['year' => '[0-9]{4}', 'month' => '0[1-9]|1[0-2]']); Route::get('/news/{year}/{month}/{slug}', $action) ->where('slug', '[a-z0-9-]+'); ``` You write only the pattern; Laravel places it inside the compiled route regex, so no delimiters are needed. ## The helper methods The helpers come from the `CreatesRegularExpressionRouteConstraints` trait, which routes, resource registrations and group registrars share. Each takes one parameter name or an array of names: | Helper | Regex it applies | Notes | |---|---|---| | `whereNumber('id')` | `[0-9]+` | any length, leading zeros allowed, no sign | | `whereAlpha('name')` | `[a-zA-Z]+` | ASCII letters only: no hyphens, digits or accents | | `whereAlphaNumeric('code')` | `[a-zA-Z0-9]+` | still no hyphens or underscores | | `whereUuid('id')` | the 8-4-4-4-12 hex pattern | either case | | `whereUlid('id')` | 26 Crockford base32 characters, first one `0`-`7` | | | `whereIn('section', [...])` | the values joined with `\|` | accepts backed enum cases too | `whereIn` joins the values into an alternation without escaping them, so a value containing a regex character, such as a dot, matches more than the literal text. Use plain slugs as values. ## What a failed constraint does Constraints are checked while the router looks for a matching route: 1. The router compares the request path with each route's compiled regex, constraints included. 2. A segment that fails its constraint makes that route a non-match, exactly as if the URI were different. 3. The router continues with the remaining routes. 4. If no route matches the path under any verb, the result is a **404 Not Found**. There is no special error for "constraint failed": no exception names the parameter, and no 422 response is produced. That is the key difference from validation. | | Route constraint | Request validation | |---|---|---| | Checks | path segments | query, body and file input | | Runs | while matching routes | inside the matched route | | Failure | route skipped, usually 404 | 422 or redirect with errors | | Purpose | route selection | rejecting bad input with feedback | ## Using constraints to separate routes In a news archive, `/news/{year}` and `/news/{slug}` look identical to the router. Constraining the first with `->where('year', '[0-9]{4}')` makes `/news/budget-vote` fail it and fall through to the slug route, while `/news/2026` matches the year route. The constrained route must be the one registered first; an unconstrained route registered earlier would still catch everything. ## Constraints beyond a single route The same helpers work wherever the constraints trait is used: - **Groups**: `Route::whereNumber('year')->prefix('news/{year}')->group(...)` constrains the parameter for every route in the group. - **Resource registrations**: a pending resource registration accepts `where()` and the helpers too, constraining the resource's parameters in one call. - **Global rules**: `Route::pattern()` applies a regex to a parameter name across the whole app. Constraining at the group level keeps a family of archive routes consistent: one change to the year rule reaches them all, and a single route can still override it with its own `where()`. ## Pitfalls interviewers probe - **`whereAlpha` on slugs.** `budget-vote` contains a hyphen, so the route 404s. Use `->where('slug', '[a-z0-9-]+')`. - **`whereNumber` as validation.** `[0-9]+` accepts `0000` and 30-digit numbers; range checks belong elsewhere. - **Expecting a cast.** A constrained parameter still arrives as a string. - **Silent 404s while debugging.** When a URL 404s unexpectedly, compare the segment with the route's constraints before suspecting the controller.

  • Why can whereIn('format', ['tar.gz', 'zip']) match more than the two listed values?
    `whereIn()` joins the values with `|` to build the regex and does not escape them. The dot in `tar.gz` is therefore a regex wildcard, so `tarXgz` also matches. Keep `whereIn` values to plain slugs, or write an explicit, escaped pattern with `where()`.
  • A teammate adds ->whereNumber('id') and calls it input validation. What do you point out?
    The constraint only decides whether the route matches: failures produce a 404 with no message, it accepts values like `0000` or 40 digits, and it does nothing for query or body input. It is a routing guard. Real rules such as ranges or existence belong in validation or in route model binding.

saying these in an interview costs you the question

  • A failed where() constraint returns a 422 validation error
  • whereAlpha accepts slugs that contain hyphens
  • whereNumber limits the value to a valid integer range
  • whereIn escapes regex characters in its values
  • A constrained parameter arrives already cast to an int