DEV Community

Kamran Khan
Kamran Khan

Posted on

My routes file is my authorisation design

Most route files I read grow into a flat list: fifty routes, middleware applied per-line, and by month three nobody can tell which endpoints are public, which need a login, and which are admin-only. In my starter kit I decided the route file should answer those questions at a glance — so I grouped routes by access level instead of by resource.

Here's the whole thing:

// Public auth endpoints
Route::prefix('auth')->group(function () {
    Route::post('register', [AuthController::class, 'register']);
    Route::post('login', [AuthController::class, 'login']);

    Route::middleware('auth:sanctum')->group(function () {
        Route::post('logout', [AuthController::class, 'logout']);
        Route::get('me', [AuthController::class, 'me']);
    });
});

// Authenticated endpoints
Route::middleware('auth:sanctum')->group(function () {
    Route::get('dashboard/stats', [DashboardController::class, 'stats']);
});

// Admin-only endpoints
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {
    Route::apiResource('users', UserController::class);
});
Enter fullscreen mode Exit fullscreen mode

The structure is three tiers, and you can read the security model without opening a single controller:

  1. Public — register and login under the auth prefix. No middleware at all.
  2. Authenticated — nested inside the auth prefix group but behind auth:sanctum. Logout and me belong here because both need a logged-in user. The dashboard stats endpoint gets its own group.
  3. Admin-only — the whole users resource (apiResource gives me index, store, show, update, destroy) behind auth:sanctum plus role:admin — my custom middleware that checks the role column on the user model.

The nesting detail that matters: logout and me sit inside the Route::prefix('auth') group, so their URLs are /api/auth/logout and /api/auth/me — auth-namespaced, but middleware-protected. Prefix is a naming decision, middleware is a security decision, and Laravel lets the two live on different nesting levels. I used that.

Why I like this for a starter kit: when you add a new endpoint, you don't ask "which middleware string do I paste" — you ask "which tier does this belong to" and drop the route into the right group. That one question replaces a whole class of mistakes. A controller can be unit-tested in isolation; the route file is the one place where the full access map is visible, so I keep it honest by keeping it small.

One convention worth copying: role:admin never runs without auth:sanctum in front of it. The role middleware assumes an authenticated user exists — pairing them in the same group makes that dependency visible and unbreakable. You can't add an admin route and forget the auth middleware, because the group carries both.


This pattern ships in my Laravel + React SaaS Starter Kit ($19 one-time) — Sanctum auth, role middleware, and the full route file above, ready to extend.

Top comments (0)