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.phpfor the app's own frontend,routes/api_v1.phpfor 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
usingcallback, Laravel ignoresweb:,api:andhealth:: it loads no route file and registers no/uproute 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 ofapiorwebmust listSubstituteBindings, 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 aRoute::{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 inRoute::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, soorder.showon the API isapi-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(). Neverauthorize()in a Form Request, and never$this->authorize()orGate::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 (SubstituteBindingsoutranksAuthorize), so a missing record is still a 404.