Routing

Route has two phases, strictly separated: registration builds a table of method/pattern/action entries; dispatch() matches the current request against that table exactly once. Every method returns self, so ->name(), ->middleware(), and ->where() chain off the registration call.

Registering routes

use Monad\Clarity\Services\Response;
use Monad\Clarity\Services\Route;

Route::get('/hello', fn () => Response::text('Hello, Monad.'));
Route::post('/hello', fn () => Response::json(['received' => true]));
Route::put('/hello/{id}', fn (string $id) => Response::json(['id' => $id]));
Route::patch(/* ... */);
Route::delete(/* ... */);
Route::options(/* ... */);

An action is a closure or a [ControllerClass::class, 'method'] pair — controller actions are plain static methods, directly callable with no resolving step. See Quickstart for a worked controller example.

Named parameters and constraints

{id} matches any non-slash segment. Typed shorthands narrow the pattern: {id:int} (\d+), {slug:alpha} ([A-Za-z]+), and {id:uuid}. A trailing ? makes a parameter optional — {id?} or {id:int?}. ->where() overrides the pattern for a specific parameter with your own regex:

Route::get('/posts/{slug:alpha}', fn (string $slug) => Response::text($slug));
Route::get('/posts/{id}', fn (?string $id) => Response::text($id ?? 'all'))->where('id', '\d{4}');

Groups

Route::group() shares a URI prefix and middleware stack across the routes registered inside its callback. Groups nest — a nested group's prefix and middleware both accumulate onto its parent's:

Route::group(['prefix' => 'admin', 'middleware' => 'App\Middlewares\Authentication'], function () {
    Route::get('/dashboard', fn () => Response::text('Admin dashboard'));

    Route::group(['prefix' => 'reports'], function () {
        Route::get('/monthly', fn () => Response::text('Monthly report'));
        // Registers GET /admin/reports/monthly, with Authentication applied.
    });
});

Named routes

Route::get('/hello/{name}', [GreetingController::class, 'show'])->name('greeting.show');

404 vs 405

dispatch() sees every registered route before deciding, so it can tell "no route matched this path" (404) apart from "a route matched this path, but not this HTTP method" (405) — something a router that matches and calls immediately on the first hit structurally cannot do, since it never examines the routes it didn't try. If nothing matches the path at all and Route::fallback() is registered, that runs instead of a bare 404:

Route::fallback(fn () => Response::text('Not Found', 404));

Next steps