skip to content

URL Mapping

Laravel maps URLs to code through route files, constrained path segments, named groups, signed links, model binding and named rate limiters. Interviewers probe what runs before a controller does.

on this pageshow

explore

questions

30

In Laravel, how does implicit route model binding turn the {task} segment of /tasks/{task} into a Task model, and what happens when no row matches?

level: juniorimportance: must knowfreq 75%

answer

  1. type-hint plus matching variable name
  2. SubstituteBindings in the web and api groups
  3. resolveRouteBinding: where route key, first()
  4. ModelNotFoundException becomes a 404
  5. wrong name gives an empty model

basics

~20 s

When an action type-hints an Eloquent model and names the variable after the route segment, Laravel queries that model by its route key (the primary key by default) and injects it. No matching row throws ModelNotFoundException, rendered as a 404.

solid answer

~40 s

The `SubstituteBindings` middleware, part of the `web` and `api` groups, inspects the action's signature. For each parameter type-hinted with an Eloquent model whose **variable name matches a segment** — `Task $task` for `{task}`, or the snake_case form, so `$projectTask` matches `{project_task}` — it calls `resolveRouteBinding($value)`, which runs `where(getRouteKeyName(), $value)->first()`; the route key is the primary key unless you change it. The model replaces the string in the route's parameters and reaches the closure or controller method. If `first()` returns null, Laravel throws `ModelNotFoundException`, which the exception handler turns into a **404**. Soft-deleted rows are excluded by the `SoftDeletes` scope. If the variable name does not match any segment, no binding happens and the container injects a new, empty `Task`.

code

php · 19 lines
php
<?php

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

Route::get('/tasks/{task}', [TaskController::class, 'show']);

// app/Http/Controllers/TaskController.php
namespace App\Http\Controllers;

use App\Models\Task;

class TaskController
{
    public function show(Task $task) // $task matches {task}
    {
        return view('tasks.show', ['task' => $task]);
    }
}

go deeper

for a junior

Recall the two conditions — a model type-hint and a variable name matching the segment — and that a missing row gives a 404.

for a middle

Explain that SubstituteBindings calls resolveRouteBinding, which queries the route key with first(), and that ModelNotFoundException becomes the 404.

for a senior

Recognise the empty-model symptom of a misnamed parameter or a route outside the binding middleware, and keep authorization separate from existence checks.

for a principal

Decide team conventions for public identifiers and binding so that lookups, 404 behaviour and authorization stay uniform across hundreds of routes.

## What implicit binding saves you Without binding, every action that shows a record starts with the same two lines: read the id from the URL, then look the row up and fail with a 404 if it is missing. **Implicit route model binding** does both for you, driven purely by the action's signature: ```php Route::get('/tasks/{task}', function (Task $task) { return view('tasks.show', ['task' => $task]); }); ``` ## The two conditions Binding happens for a parameter only when both hold: 1. **The type-hint is an Eloquent model** (more precisely, a class implementing `Illuminate\Contracts\Routing\UrlRoutable`, which every Eloquent model does). 2. **The variable name matches a route segment**: `$task` for `{task}`. Laravel also accepts the snake_case form of the variable name, so `$projectTask` binds `{project_task}`. The position of the parameter does not matter, and other injected services can sit beside it. ## What runs, step by step The work is done by the `SubstituteBindings` middleware, which the default `web` and `api` middleware groups include. For each bindable top-level parameter it: 1. Creates an instance of the model class. 2. Calls `resolveRouteBinding($value, $field)` on it. The default implementation is `where($field ?? $this->getRouteKeyName(), $value)->first()`. 3. If a model comes back, replaces the raw string in the route's parameters with it. 4. If `null` comes back, throws `Illuminate\Database\Eloquent\ModelNotFoundException`. `getRouteKeyName()` returns the primary key name (`id`) unless the model changes it, so `/tasks/7` runs roughly `select * from tasks where id = '7' limit 1`. (A nested child on a route with scoped bindings is resolved through its parent's relationship instead; `/tasks/{task}` has no parent.) ## What happens when nothing matches `ModelNotFoundException` is not caught by your action; Laravel's exception handler converts it to a `NotFoundHttpException`, so the user sees the standard **404** page (or a JSON 404 for API requests). Two related cases: - **Soft-deleted rows** are invisible by default, because the `SoftDeletes` global scope adds `deleted_at is null`. A soft-deleted task therefore 404s too, unless the route opts in with `->withTrashed()`. - **A backed-enum type-hint** is bound the same way: a segment that is not a valid case of the enum also produces a 404. ## The two silent failure modes | Mistake | What the action receives | |---|---| | Variable named differently from the segment (`Task $item` for `{task}`) | A **new, empty `Task`** built by the container: `exists` is false and every attribute is null | | Route registered outside the `web`/`api` groups, so `SubstituteBindings` never runs | Again an empty model from the container, while the raw id sits unused in the route parameters | Both look like "the model has no data" rather than an error, which is why they cost debugging time. The fix is always to align the name or make sure the binding middleware runs. ## Where binding sits in the request Binding happens in middleware, before the action runs. That has practical consequences: - Middleware that runs **before** `SubstituteBindings` sees the raw string: `$request->route('task')` returns `"7"`. Middleware that runs **after** it sees the `Task` model. - Each bound parameter costs **one query**. The model arrives without relations; load what the view needs in the action with `$task->load('assignee')` rather than lazily in a loop. - Binding works the same for closure routes and controller methods, and the model is also available to form requests and policies that read the route parameter later. - A failed binding stops the request before any controller code, so the action can assume the model exists. ## Why it is worth using - **Less code, fewer inconsistencies**: every task route 404s the same way. - **One place to change the lookup**: the column, soft-delete handling and scoping are declared on the route or the model instead of repeated in each action. - **Links follow the same key**: passing the model to `route()` uses its route key, so lookups and generated URLs agree. Implicit binding only answers "does this record exist?". It does **not** answer "may this user see it?" — authorization is a separate step.

  • A controller method declares show(Task $item) for the route /tasks/{task}. What does $item contain?
    A new, empty `Task`. Binding matches on the variable name, so `$item` is not tied to `{task}`; the router then resolves the type-hinted class from the container, which instantiates a fresh model with `exists` false. Rename the parameter to `$task` to get the bound record.
  • How does implicit binding treat a PHP backed enum type-hinted in a route action?
    Laravel calls the enum's `tryFrom()` with the segment value and injects the case. If no case matches, it throws `BackedEnumCaseNotFoundException`, which the exception handler renders as a 404 — so `/tasks/status/{status}` with `TaskStatus $status` only reaches the action for valid values.

saying these in an interview costs you the question

  • Binding works by type-hint alone, whatever the variable is called
  • A missing row injects null into the action
  • A missing model produces a 500 error
  • Implicit binding checks that the user may view the record
  • Soft-deleted rows are bound like any other row
open as a page

In Laravel, how do you name a route with ->name() and build its URL with route(), including parameters and extra query keys?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Chain ->name('franchise.orders.show') on the route, then call route('franchise.orders.show', ['order' => 42]); matching keys fill the placeholders, leftover keys become the query string, and the URL is absolute unless the third argument is false.

open as a page

In Laravel, how do you declare required and optional route parameters, and what must the handler supply for an optional one?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Wrap a URI segment in braces, {year}, to capture it, and add a question mark, {month?}, to make it optional; the handler argument for an optional segment needs a default value, such as ?string $month = null.

open as a page

In Laravel, which HTTP verbs do Route::get, Route::match and Route::any register, and how do you confirm them?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Route::get registers GET and HEAD; post, put, patch, delete and options register one verb each; Route::match registers the verbs you list (plus HEAD when GET is listed); Route::any registers all seven router verbs. php artisan route:list shows each route's verbs, URI, name and action.

open as a page

In a Laravel 13 app, how do routes/web.php and routes/api.php differ, and why does a new app have no api.php?

level: juniorimportance: must knowfreq 70%

basics

~10 s

routes/web.php is loaded inside the web middleware group for browser pages with sessions and CSRF protection; routes/api.php, created by php artisan install:api, is loaded inside the stateless api group under an automatic /api prefix.

open as a page

In Laravel, how do nested route groups combine their prefix, name, middleware, where, controller and domain attributes?

level: middleimportance: must knowfreq 55%

basics

~20 s

Nested Laravel groups append prefixes with a slash and concatenate name prefixes exactly as written, merge middleware (outer first) and where constraints (inner wins on a clash), and let an inner controller or domain replace the outer one.

open as a page

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%

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.

open as a page

In Laravel, how do you define a named rate limiter with RateLimiter::for and Limit, key it per user with by(), and attach it with throttle:name?

level: middleimportance: must knowfreq 60%

basics

~10 s

Register RateLimiter::for('weather-api', fn (Request $r) => Limit::perMinute(60)->by($r->user()?->id ?: $r->ip())) in AppServiceProvider::boot(), then add ->middleware('throttle:weather-api') to routes. Over the limit, Laravel answers 429 with Retry-After.

open as a page

In a Laravel route /projects/{project}/tasks/{task}, why can a user load another project's task, and how do scopeBindings() and withoutScopedBindings() change that?

level: seniorimportance: must knowfreq 45%

basics

~20 s

By default each implicit binding is resolved on its own, so /projects/1/tasks/99 loads task 99 even if it belongs to project 2. scopeBindings() resolves the task through $project->tasks(), turning a mismatch into a 404; withoutScopedBindings() turns that scoping off.

open as a page

In Laravel, how do you check which named route handled the current request, for example to highlight the active admin menu item?

level: juniorimportance: should knowfreq 45%

basics

~10 s

Use request()->routeIs('admin.orders.'), which matches the current route's name with * wildcards, or Route::currentRouteName() for the name itself; request()->is('admin/') checks the path instead, and both name checks are false or null when no route matched.

open as a page

In Laravel, how does RateLimiter::attempt() limit an arbitrary action such as sending an SMS weather alert, and what does it return?

level: juniorimportance: should knowfreq 30%

basics

~20 s

RateLimiter::attempt($key, $maxAttempts, $callback, $decaySeconds = 60) runs the callback and counts a hit only while attempts remain. It returns false when the limit is reached, otherwise the callback's return value, or true if that is null.

open as a page

In Laravel, what do Route::view and Route::redirect register, and what defaults and reserved parameter names do they carry?

level: juniorimportance: should knowfreq 35%

basics

~10 s

Route::view registers a GET/HEAD route that renders a Blade view with optional data; Route::redirect registers a route for every verb that redirects with 302 unless you pass a status; Route::permanentRedirect sends 301.

open as a page

In Laravel 13, how do {project:slug}, getRouteKeyName() and the #[RouteKey] attribute change which column route model binding queries?

level: middleimportance: should knowfreq 50%

basics

~20 s

{project:slug} makes one route query the slug column. getRouteKeyName() sets the default column for every binding of that model; in Laravel 13 the #[RouteKey('slug')] class attribute does the same declaratively. Without either, the primary key is used.

open as a page

In Laravel, when would you use Route::model(), Route::bind() or an overridden resolveRouteBinding() instead of plain implicit route model binding?

level: middleimportance: should knowfreq 35%

basics

~20 s

Route::model() binds a segment name to a model class even without a type-hint; Route::bind() runs your own closure for a segment and can return anything; overriding resolveRouteBinding() changes the lookup for every top-level binding of that model.

open as a page

In Laravel, how do withTrashed(), missing() and a backed-enum route parameter change what a bound route does when the lookup fails or finds a soft-deleted row?

level: middleimportance: should knowfreq 30%

basics

~20 s

Soft-deleted models 404 unless the route calls withTrashed(). missing() swaps the not-found 404 for your own response, such as a redirect. A backed-enum parameter binds only valid cases; anything else is a 404 that missing() does not intercept.

open as a page

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%

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.

open as a page

In a Laravel named rate limiter, what do after() and response() change about which requests count and what a throttled client receives?

level: middleimportance: should knowfreq 30%

basics

~20 s

after() takes a closure that receives the response and returns true when that response should count, so only chosen outcomes use up the limit. response() replaces the default 429 with your own response built from the request and the rate-limit headers.

open as a page

In Laravel, what does Route::fallback() do, where should you define it, and what are its limits?

level: middleimportance: should knowfreq 35%

basics

~20 s

Route::fallback() registers a GET/HEAD route that runs only when no other route matches; defined in routes/web.php it gets the web middleware group, it is checked last wherever it is declared, and its response status is whatever the action returns.

open as a page

In a Laravel Blade form, why do you add @method('DELETE'), and what breaks when that spoofing is set up wrong?

level: middleimportance: should knowfreq 50%

basics

~20 s

HTML forms can only send GET or POST, so @method('DELETE') adds a hidden _method field; Laravel reads it on a POST request and routes the request as DELETE, which lets a form reach a Route::delete route.

open as a page

How does Laravel's Route::domain() serve a franchise subdomain such as {franchise}.example.com, and what changed about domain routes in Laravel 13?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Route::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.

open as a page

In Laravel, what happens when two routes share a name, and how does that surface differently with and without route caching?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Without caching, Laravel's name lookup keeps just one of the routes, so route() silently points at it while the other stays reachable only by URL; route:cache refuses the duplicate with a LogicException naming the URI and the name.

open as a page

A Laravel news-archive route injects a service beside {year} and {month?}, and the handler gets swapped values or crashes when month is absent; why, and how do you fix it?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Laravel passes route values to the handler by position and splices container-resolved services in; an absent optional segment is dropped, so a missing default crashes. List services first, then parameters in URI order, optional ones last with defaults.

open as a page

Where does Laravel store rate-limiter counters, and when would you set the cache.limiter key or call throttleWithRedis() in bootstrap/app.php?

level: seniorimportance: should knowfreq 25%

basics

~10 s

RateLimiter keeps counters in the cache store named by cache.limiter, or the default store when that key is absent. throttleWithRedis() in bootstrap/app.php maps the throttle alias to ThrottleRequestsWithRedis, which counts directly in Redis.

open as a page

For a Laravel weather-data API with per-plan quotas, how would you combine Limit::perSecond, Limit::perDay, limit arrays and Limit::none() in one named limiter?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Have one RateLimiter::for closure read the user's plan and return an array such as [Limit::perSecond(20)->by('s:'.$id), Limit::perDay(100_000)->by('d:'.$id)], or Limit::none() for unlimited plans. Every limit in the array must pass.

open as a page

In Laravel, how do you let a route parameter capture slashes, such as a nested topic path, and what limits apply?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

A route parameter matches everything except a slash by default; ->where('path', '.*') lets it capture slashes, and the routing docs support that only when the parameter is the last segment of the URI.

open as a page

In Laravel 13's bootstrap/app.php, how do you register an extra route file, and how do withRouting's then and using arguments differ?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Pass extra files to withRouting's web: or api: arrays, or register them in a then: closure that runs after the default files; a using: closure replaces the framework's registration entirely, so web:, api: and health: are no longer registered for you.

open as a page