The simplest route is a verb, a URI and a closure. The Route facade has get, post, put, patch, delete and options; match() takes a list of verbs and any() accepts all of them. A returned string is sent as HTML; an array, model or collection becomes JSON. Type-hint Request, or any other class, and the service container passes it in.
Route::get('/', fn () => 'Welcome to the bookshop');
Route::get('/products', function () {
return Product::orderBy('id')->get(['id', 'sku', 'name', 'price']);
})->name('products.index');
Route::match(['get', 'post'], '/newsletter', function (Request $request) {
return $request->method() . ' /newsletter';
});$ curl -s http://127.0.0.1:8303/products | jq -c '.[]'
{"id":1,"sku":"BK-1001","name":"Learning SQL","price":39.99}
{"id":2,"sku":"BK-1002","name":"Modern PHP","price":34.5}
{"id":3,"sku":"BK-2001","name":"Dune","price":12.99}
$ curl -i -X POST http://127.0.0.1:8303/newsletter
HTTP/1.1 419 unknown status
...
<title>Page Expired</title>Every get route also answers HEAD. The POST failed with 419, "Page Expired": routes in web.php run the web middleware group, whose CSRF check rejects a state-changing request without a token (CSRF Protection). A form sends the token with @csrf, and spoofs PUT or DELETE with @method('DELETE'). PHP's built-in server prints "unknown status" because 419 is not a registered HTTP code. Routes are tried in registration order, so put single-verb routes before any() or match() routes on the same URI.