Cleaner API Routes with Consistent Grouping

πŸ“– 3 minutes read

When your Laravel routes file starts getting messy with scattered middleware, repeated patterns, and verbose resource declarations, it’s time to refactor. Here are three patterns that will make your API routes cleaner and more maintainable.

1. Group by Middleware, Then by Prefix and Name

Instead of scattering ->middleware('auth:sanctum') across individual routes, group authenticated routes together first:

// ❌ Before: middleware scattered everywhere
Route::post('logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
Route::get('profile/notifications', [NotificationController::class, 'index'])->middleware('auth:sanctum');
Route::post('reports/{report}/approve', [ReportController::class, 'approve'])->middleware('auth:sanctum');

// βœ… After: group by middleware first
Route::middleware('auth:sanctum')->group(function () {
    Route::prefix('auth')->name('auth.')->group(function () {
        Route::post('logout', [AuthController::class, 'logout'])->name('logout');
    });
    
    Route::prefix('profile')->name('profile.')->group(function () {
        Route::apiResource('notifications', NotificationController::class)->only(['index', 'show']);
    });
    
    Route::prefix('report/{report}')->name('report.')->group(function () {
        Route::post('approve', [ReportController::class, 'approve'])->name('approve');
    });
});

This groups all protected routes in one middleware wrapper, then organizes them by prefix and route name. Much easier to scan and understand the structure.

2. Consolidate Multiple apiResource Calls

If you have several resources that share the same configuration, use apiResources() (plural) instead of repeating the same options:

// ❌ Before: repetitive
Route::apiResource('categories', CategoryController::class)->except(['store', 'update', 'destroy']);
Route::apiResource('tags', TagController::class)->except(['store', 'update', 'destroy']);
Route::apiResource('products', ProductController::class)->except(['store', 'update', 'destroy']);
Route::apiResource('reviews', ReviewController::class)->except(['store', 'update', 'destroy']);

// βœ… After: consolidated
Route::apiResources([
    'categories' => CategoryController::class,
    'tags' => TagController::class,
    'products' => ProductController::class,
    'reviews' => ReviewController::class,
], ['except' => ['store', 'update', 'destroy']]);

This reduces 8 lines to 6, and makes it crystal clear that these are all public read-only resources with the same access rules.

3. Move Route Parameters to the Prefix

When multiple routes operate on the same parent resource, move the parameter to the prefix instead of repeating it in every URI:

// ❌ Before: {task} repeated in every route
Route::post('tasks/{task}/comments', [TaskController::class, 'comment']);
Route::post('tasks/{task}/assign', [TaskController::class, 'assign']);
Route::post('tasks/{task}/complete', [TaskController::class, 'complete']);

// βœ… After: parameter in prefix
Route::prefix('task/{task}')->name('task.')->middleware('auth:sanctum')->group(function () {
    Route::post('comment', [TaskController::class, 'comment'])->name('comment');
    Route::post('assign', [TaskController::class, 'assign'])->name('assign');
    Route::post('complete', [TaskController::class, 'complete'])->name('complete');
});

Now the URIs are cleaner (/task/123/comment), the middleware is declared once, and the route names follow a consistent pattern (task.comment, task.assign, etc.).

The Grouping Order That Works

Apply grouping in this order for maximum readability:

  1. Middleware β€” auth, guest, throttle, etc.
  2. Prefix β€” URL segment grouping
  3. Name β€” route name prefix

Following this pattern consistently across your routes file makes it much easier to scan, modify, and maintain as your API grows.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *