laravel-routing

Installation
SKILL.md

Laravel Routing

A surface is one consumer of the app: its own frontend, or a versioned public API. Each surface has one route file, registered in bootstrap/app.php with its middleware, prefix and name, and its controllers live under its own namespace. The route file is then the complete map of that surface: every endpoint written out with its verb, its public URL, its name and the ability a caller needs.

Rules

  • One route file per surface. routes/web.php for the app's own frontend, routes/api_v1.php for version 1 of an API. Controllers are namespaced to match: App\Http\Api\V1\Controllers. A breaking API change is a new surface beside the old one (laravel-architecture).
  • Surfaces are registered in the withRouting() callback. One chain per surface, Route::middleware()->domain()->prefix()->name()->group(base_path(...)) (domain only when the surface has one), with a short comment above it saying who the surface serves. The surface's prefix, name prefix and middleware sit there and nowhere else; the route file never repeats them.
  • The callback replaces the named arguments. With a using callback, Laravel ignores web:, api: and health:: it loads no route file and registers no /up route on its own. Register everything inside the callback.
  • A surface's own middleware is a named group in the MiddlewareBooter (laravel-providers), stacked after the stock group: Route::middleware(['api', 'api_v1']). A group used instead of api or web must list SubstituteBindings, or no route on the surface binds its models.
  • Authentication sits in the route file. Route::middleware('auth:sanctum')->group(...) wraps the routes that need it, so a public login or token route can live in the same file.
  • One explicit route per endpoint, never Route::resource(). Each endpoint is a Route::{verb}() with its own ->can() and ->name(), so its verb, path, ability and name read together and a search finds it. The routes for one resource sit in Route::prefix('orders')->group(...), nested resources included.
  • Names are {resource}.{action}, singular kebab-case. order.show, order.cancel, order.resend-confirmation. A nested resource is {parent}-{child}.{action}: order-note.store. Write each route's full name; the surface's prefix comes from its group, so order.show on the API is api-v1.order.show. URLs stay plural: /orders/{order:ulid}.
  • Bind by the public column. {order:ulid}, never the auto-increment id; laravel-models owns which column a model has. A child bound by its column is scoped to its parent: /orders/{order:ulid}/notes/{note:ulid} resolves through $order->notes(), so another order's note is a 404.
  • Authorization lives on the route. Every route on a surface with a permission model carries ->can(). Never authorize() in a Form Request, and never $this->authorize() or Gate::authorize() in a controller. ->can() is route middleware, so it runs before the controller and its Form Request are resolved: an unauthorized caller gets a 403, not a 422 listing what's wrong with their input, and no validation query runs. Bindings resolve first (SubstituteBindings outranks Authorize), so a missing record is still a 404.

Verbs

Installs
56
GitHub Stars
15
First Seen
Mar 27, 2026