M
MeshWorld.
Laravel Routing PHP Eloquent 5 min read

Route Model Binding in Laravel Explained

Vishnu
By Vishnu
| Updated: Mar 27, 2026

Route model binding in Laravel automatically resolves Eloquent models from route parameters without manual findOrFail() calls. When you type-hint a model in a controller method, Laravel queries the database and injects the matching record. If nothing is found, it throws a 404 automatically. No boilerplate. No repetitive lookup code. Works in Laravel 10, 11, and 12.

:::note[TL;DR]

  • Type-hint a model in your controller and Laravel injects it automatically from the route {param}
  • Default binding resolves by id; override with {post:slug} or getRouteKeyName()
  • Scoped bindings enforce parent-child ownership at the routing layer — no manual check needed
  • ->withTrashed() makes soft-deleted records resolvable instead of 404ing
  • Wrong model relationship setup (hasMany/belongsTo) causes scoped binding to silently skip the constraint :::

How does implicit route model binding work?

Laravel reads the type-hint in your controller method and matches it to the {param} name in the route. Same name, same model — Laravel connects them.

Define the route:

// routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show']);

Type-hint the model in the controller:

// app/Http/Controllers/PostController.php
public function show(Post $post): View
{
    return view('posts.show', compact('post'));
}

Laravel sees Post $post, finds {post} in the URL, runs Post::findOrFail($id) under the hood, and injects the resolved model. If no record matches, 404. Done.

The Scenario: You’re building a blog and every show() method starts with $post = Post::findOrFail($id). Same line, dozens of controllers, same boilerplate every time. Route model binding kills that line entirely. The model is just there — already resolved, 404 handled, ready to use.

How do you bind by a custom key like a slug?

Two ways. Pick the one that fits your app’s style.

Option A — inline key in the route. Most explicit. Easy to read at a glance:

// routes/web.php
Route::get('/posts/{post:slug}', [PostController::class, 'show']);

Laravel resolves Post using WHERE slug = ? instead of WHERE id = ?. No model changes needed.

Option B — override getRouteKeyName() in the model. Sets a global default for every route that binds this model:

// app/Models/Post.php
public function getRouteKeyName(): string
{
    return 'slug';
}

Option A is better in large apps where different routes for the same model might use different keys. Option B is cleaner when every route for that model always uses the same custom key.

If you’re also using route model binding alongside composite indexes, make sure the custom key column is indexed — otherwise you’re just doing a slow full table scan on every request.

How do scoped bindings enforce parent-child ownership?

Scoped bindings add a WHERE parent_id = ? constraint automatically. No manual ownership check in the controller.

This route ensures $post belongs to $user. If a valid {post:slug} exists but belongs to a different user, Laravel returns 404:

// routes/web.php

// Ensures $post actually belongs to $user — throws 404 if not
Route::get('/users/{user}/posts/{post:slug}', function (User $user, Post $post) {
    return view('posts.show', compact('post'));
});

For scoping to work, Post must have a belongsTo(User::class) relationship and User must have hasMany(Post::class). Laravel uses those to build the scoped query.

The Scenario: Your app has user-specific resources. A logged-out attacker guesses a valid slug and hits /users/2/posts/some-slug. Without scoped binding, they’d get the post regardless of which user it belongs to. With it, the route enforces ownership before the controller even runs. One line of config replaces a manual authorization check.

How do you resolve soft-deleted records?

By default, soft-deleted records return 404. Add ->withTrashed() to the route when you need them to resolve:

// routes/web.php
Route::get('/admin/posts/{post}', function (Post $post) {
    return view('admin.posts.show', compact('post'));
})->withTrashed();

Use this for admin views where a moderator needs to review or restore deleted content. Don’t add it to public routes — that exposes records your users expect to be gone.

:::warning Scoped bindings require the child parameter to use {param:key} syntax or getRouteKeyName(). If the parent/child Eloquent relationship (hasMany, belongsTo) isn’t defined correctly, scoping silently falls back to unscoped behaviour in some Laravel versions — no error thrown, no 404, just the wrong record. Always verify your model relationships before relying on this in production. :::

Summary

  • Implicit binding resolves Eloquent models from route parameters automatically — type-hint the model and Laravel handles the rest
  • Custom keys: use {post:slug} inline or override getRouteKeyName() on the model
  • Scoped bindings add a parent ownership constraint at the routing layer; requires correct hasMany/belongsTo setup
  • ->withTrashed() makes soft-deleted records resolvable on a per-route basis

FAQ

Does route model binding work with API routes? Yes. It works identically in routes/api.php. The only difference is API routes go through the api middleware group by default.

What happens if the {param} name doesn’t match the variable name in the controller? Nothing resolves. Laravel matches by name. If the route has {post} and the controller has Post $article, the binding fails and $article gets the raw string value from the URL instead of a model.

Can I bind multiple models in one route? Yes. Each type-hinted parameter gets resolved independently. For nested resources, use scoped bindings to enforce ownership between them.

Does custom key binding work with firstOrCreate? Not directly. firstOrCreate is a model method, not a routing feature. For a full overview of that method, see firstOrCreate model method in Laravel.

Can I throw a custom exception instead of a 404? Yes. Override resolveRouteBinding() on the model and throw whatever you need. The default behaviour calls findOrFail() which throws ModelNotFoundException, caught by Laravel’s exception handler as a 404.