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:

MethodURIController MethodMiddleware
GET/postsindexcan:list,Post
GET/posts/{post}showcan:show,post
POST/postsstorecan:store,Post
PUT/posts/{post}updatecan:update,post
DELETE/posts/{post}destroycan:destroy,post

Clean, right?


How It Works

Here’s what the macro does behind the scenes:

  1. 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.

Categorized in: