When building APIs or web apps with Laravel, defining CRUD (Create, Read, Update, Delete) routes is a common and repetitive task. Each resource usually requires 5-7 routes, often paired with policy middleware. Wouldn’t it be great if you could reduce all of that to one clean line of code?
Well, you can — with a custom Route::crud()
macro.
What Is a Route Macro?
Laravel allows you to extend the routing system using macros. A route macro is a reusable function that registers routes in a specific pattern. By defining a Route::crud()
macro, we can automate the process of generating standard CRUD routes along with middleware for authorization.
The Custom Route::crud()
Macro
Here’s what this macro lets you do in your routes file:
Route::crud('posts', PostController::class, \App\Models\Post::class);
That single line will generate:
Method | URI | Controller Method | Middleware |
---|---|---|---|
GET | /posts | index | can:list,Post |
GET | /posts/{post} | show | can:show,post |
POST | /posts | store | can:store,Post |
PUT | /posts/{post} | update | can:update,post |
DELETE | /posts/{post} | destroy | can:destroy,post |
Clean, right?
How It Works
Here’s what the macro does behind the scenes:
- Infers Route Parameter Name
If you pass'posts'
, it automatically uses'post'
for the parameter:
/posts/{post}
Defines CRUD Actions
It maps each action (index
, show
, etc.) to the appropriate HTTP method and route.
Attaches Policy Middleware
It uses Laravel’s can:
middleware to authorize each action, automatically determining whether to pass a model class or route parameter:
can:list,Post
can:show,post
Supports Customization
You can choose which actions to include or exclude:
Route::crud('comments', CommentController::class, Comment::class, only: ['index', 'store']);
How to Register the Macro
You can define the macro in your RouteServiceProvider
or a separate service provider:
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
Route::macro('crud', function ($route, $class, $modelClass, $only = ['index', 'show', 'store', 'update', 'destroy'], $except = [], $paramName = null) {
if (!$paramName) {
$paramName = lcfirst(
Str::studly(
Str::singular(
str_replace('/', '', $route)
)
)
);
}
$singularRoute = $route . '/{' . $paramName . '}';
$actions = [
'index' => ['method' => 'get', 'route' => $route, 'action' => 'index'],
'show' => ['method' => 'get', 'route' => $singularRoute, 'action' => 'show'],
'store' => ['method' => 'post', 'route' => $route, 'action' => 'store'],
'update' => ['method' => 'put', 'route' => $singularRoute, 'action' => 'update'],
'destroy' => ['method' => 'delete', 'route' => $singularRoute, 'action' => 'destroy']
];
$middlewares = [
'index' => 'list',
'show' => 'show',
'store' => 'store',
'update' => 'update',
'destroy' => 'destroy',
];
return Route::group([], function () use ($only, $actions, $class, $middlewares, $paramName, $except, $modelClass) {
if (!empty($except)) {
$except = collect($except);
$only = collect($only)->filter(fn ($key) => !$except->contains($key))->all();
}
foreach ($only as $item) {
$action = $actions[$item];
$bindActionsWithClass = collect(['index', 'store']);
$middlewareBinding = $bindActionsWithClass->contains($action['action'])
? $modelClass
: $paramName;
Route::{$action['method']}(
$action['route'],
[$class, $action['action']]
)->middleware("can:{$middlewares[$action['action']]},$middlewareBinding");
}
});
});
Real-Life Scenario: Building a Blog CMS
Let’s say you’re developing a blog platform where users can manage:
- Posts
- Comments
- Categories
- Tags
Each of these resources needs basic CRUD functionality — and you want to secure access using Laravel’s policy system.
Goal
Create all CRUD routes for posts
, comments
, and categories
with appropriate authorization middleware using minimal code.
Example: Post Management
1. Model & Controller
php artisan make:model Post -m
php artisan make:controller PostController --api
php artisan make:policy PostPolicy --model=Post
2. PostController
class PostController extends Controller
{
public function index() { return Post::all(); }
public function show(Post $post) { return $post; }
public function store(Request $request) {
$validated = $request->validate(['title' => 'required']);
return Post::create($validated);
}
public function update(Request $request, Post $post) {
$post->update($request->all());
return $post;
}
public function destroy(Post $post) {
$post->delete();
return response()->noContent();
}
}
3. Policy (PostPolicy)
class PostPolicy
{
public function list(User $user) {
return $user->hasPermission('view_posts');
}
public function show(User $user, Post $post) {
return $user->id === $post->user_id;
}
public function store(User $user) {
return $user->hasPermission('create_posts');
}
public function update(User $user, Post $post) {
return $user->id === $post->user_id;
}
public function destroy(User $user, Post $post) {
return $user->id === $post->user_id;
}
}
4. Route Definition Using the Macro
use App\Http\Controllers\PostController;
use App\Models\Post;
Route::middleware(['auth:sanctum'])->group(function () {
Route::crud('posts', PostController::class, Post::class);
});
What This Does:
Generates 5 routes:
GET /posts → index
GET /posts/{post} → show
POST /posts → store
PUT /posts/{post} → update
DELETE /posts/{post} → destroy
Each route is automatically protected by can:
middleware:
can:list,App\Models\Post
can:show,post
- etc.
Want Fewer Routes?
Maybe your comments
resource only needs index
and store
. No problem:
Route::crud('comments', CommentController::class, Comment::class, ['index', 'store']);
Want to Exclude a Route?
Don’t want users to destroy
a category?
Route::crud('categories', CategoryController::class, Category::class, only: ['index', 'store', 'update', 'show'], except: ['destroy']);
Real-World Benefits
- Security built-in via policy binding
- Fewer lines of code in
routes/api.php
- Consistent standards across your application
- Easy maintenance and updates
Wrap-Up
This Route::crud()
macro is a powerful tool to streamline and secure your Laravel route definitions. It’s ideal for teams and projects aiming for clean code, standardization, and maintainability.
Pro Tip: Pair this with Laravel policies for complete control over who can access each route.
Comments