Resource controllers and Route::resource()
Concept
Route::resource() generates seven conventional routes for a CRUD resource in a single line, eliminating the boilerplate of registering index, create, store, show, edit, update, and destroy individually. This follows the REST convention that HTTP verbs combined with URI patterns express intent: GET /users lists, POST /users creates, GET /users/{user} shows, and so on.
| Action | Verb | URI | Controller Method | Route Name |
|---|---|---|---|---|
| List | GET | /users | index | users.index |
| Create form | GET | /users/create | create | users.create |
| Store | POST | /users | store | users.store |
| Show | GET | /users/{user} | show | users.show |
| Edit form | GET | /users/{user}/edit | edit | users.edit |
| Update | PUT/PATCH | /users/{user} | update | users.update |
| Destroy | DELETE | /users/{user} | destroy | users.destroy |
Internally, Router::resource() creates a Illuminate\Routing\ResourceRegistrar instance and calls register(). The registrar maps each action to a route via addResourceIndex(), addResourceCreate(), etc. These methods call the underlying Route::get/post/put/patch/delete methods just as you would manually, injecting the standard parameter name (singularized resource name) into each parameterized route.
Limiting generated routes is done with ->only(['index', 'show']) or ->except(['create', 'edit']) for API resources that don't serve HTML forms. Naming overrides use ->names(['index' => 'users.list']). Parameter naming overrides use ->parameters(['users' => 'account']) to change {user} to {account}.
Nested resources — Route::resource('users.posts', PostController::class) — generate routes with both {user} and {post} parameters, producing URIs like /users/{user}/posts/{post}. Shallow nesting is preferred for deeply nested resources: ->shallow() generates full nested URIs for collection operations (index, create, store) but flat URIs for single-resource operations (show, edit, update, destroy).
Code Example
<?php
use App\Http\Controllers\UserController;
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
// Full resource — all 7 routes
Route::resource('users', UserController::class);
// Limit to specific actions
Route::resource('users', UserController::class)->only(['index', 'show']);
Route::resource('users', UserController::class)->except(['create', 'edit']);
// Override parameter name: /users/{account} instead of /users/{user}
Route::resource('users', UserController::class)
->parameters(['users' => 'account']);
// Override generated names
Route::resource('users', UserController::class)
->names(['index' => 'users.list', 'show' => 'users.detail']);
// Nested resource: /users/{user}/posts/{post}
Route::resource('users.posts', PostController::class);
// Shallow nesting — flat URIs for single-resource operations
Route::resource('users.posts', PostController::class)->shallow();
// GET /users/{user}/posts => posts.index
// POST /users/{user}/posts => posts.store
// GET /posts/{post} => posts.show
// PUT /posts/{post} => posts.update
// DELETE /posts/{post} => posts.destroy
// Inside a group with name prefix
Route::prefix('/admin')->name('admin.')->group(function () {
Route::resource('users', UserController::class);
// Produces: admin.users.index, admin.users.show, etc.
});
// The corresponding controller
class UserController extends Controller
{
public function index(): View { /* list all users */ }
public function create(): View { /* show create form */ }
public function store(Request $request): RedirectResponse { /* persist */ }
public function show(User $user): View { /* show one user */ }
public function edit(User $user): View { /* show edit form */ }
public function update(Request $request, User $user): RedirectResponse { /* update */ }
public function destroy(User $user): RedirectResponse { /* delete */ }
}Interview Q&A
Q: Why does Route::resource() register both PUT and PATCH for the update action, and what is the semantic difference between them?
Route::resource() registers update for both PUT and PATCH because browsers can only submit GET and POST forms natively. Laravel uses a hidden _method field (method spoofing) so a POST with _method=PUT or _method=PATCH is treated as the corresponding verb. The semantic distinction: PUT means replace the entire resource (you must send all fields), PATCH means partial update (send only changed fields). Laravel's resource controller doesn't enforce this semantically — the controller method handles both verbs — but the distinction matters for API clients that send proper HTTP requests.
Q: What does ->shallow() do and when should you use it?
->shallow() generates full nested URIs for collection-level routes — where you need the parent context to create or list children (GET /users/{user}/posts, POST /users/{user}/posts) — but flat URIs for member-level routes, because the child's own ID is sufficient to locate it (GET /posts/{post}, DELETE /posts/{post}). Use it when your child resource has a globally unique ID (auto-increment or UUID), which is the common case. Deep nesting like /users/{user}/posts/{post}/comments/{comment} becomes unwieldy and breaks REST conventions; shallow nesting solves this ergonomically.
Q: How do you add a non-standard route to a resource controller beyond the seven defaults?
Register the extra route before Route::resource() so it takes precedence over the resource's parameterized routes. For example, to add GET /users/export, place Route::get('/users/export', [UserController::class, 'export'])->name('users.export') before Route::resource('users', UserController::class). If placed after, /users/{user} with user=export would match first and attempt to bind export as a model key. For many extra routes, consider breaking the resource up or using apiResource() combined with a separate route group.