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);
});
The structure is three tiers, and you can read the security model without opening a single controller:
-
Public — register and login under the
authprefix. No middleware at all. -
Authenticated — nested inside the
authprefix group but behindauth:sanctum. Logout andmebelong here because both need a logged-in user. The dashboard stats endpoint gets its own group. -
Admin-only — the whole
usersresource (apiResourcegives me index, store, show, update, destroy) behindauth:sanctumplusrole:admin— my custom middleware that checks therolecolumn 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)