Originally published on laravel.learnio.dev — part of the Laravel best practices course. View this lesson on the original site.
Turn URLs and verbs into clear contracts
Routes define your app's public HTTP surface. Every incoming request is matched by method and path before Laravel calls your code.
For most web pages, start in routes/web.php. These routes use the web middleware group (sessions, cookies, CSRF protection).
API endpoints usually live in routes/api.php.
Register routes by HTTP method
Laravel supports all common HTTP verbs:
use Illuminate\Support\Facades\Route;
Route::get('/products', fn () => 'List products');
Route::post('/products', fn () => 'Create product');
Route::put('/products/{id}', fn (string $id) => "Replace product {$id}");
Route::patch('/products/{id}', fn (string $id) => "Update product {$id}");
Route::delete('/products/{id}', fn (string $id) => "Delete product {$id}");
Use the verb that matches your intent:
GETfor reads,POSTfor creates,PUTorPATCHfor updates,DELETEfor removals.
Route to a controller action
Closures are fine for simple checks, but app code should usually go through controllers:
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;
Route::get('/products', [ProductController::class, 'index']);
Route::post('/products', [ProductController::class, 'store']);
This keeps route files declarative and pushes logic into testable classes.
Bad way vs better way
Bad way (mixed intent and wrong verb semantics):
Route::any('/products', function () {
// create, update, delete, and list all in one place
});
Better way (clear verb-specific routes):
Route::get('/products', [ProductController::class, 'index']);
Route::post('/products', [ProductController::class, 'store']);
Route::patch('/products/{product}', [ProductController::class, 'update']);
Route::delete('/products/{product}', [ProductController::class, 'destroy']);
Why better:
- each endpoint has one job,
- access control and validation are easier to reason about,
- tests can target one behaviour at a time.
Name routes early
Named routes make URL generation and redirects reliable:
Route::get('/products/{id}', [ProductController::class, 'show'])
->name('products.show');
Then in code:
return redirect()->route('products.show', ['id' => $productId]);
If the path changes later, your route name stays stable.
Check your routing table often
Use this command while building:
php artisan route:list
Useful filters:
php artisan route:list --method=GET
php artisan route:list --name=products
php artisan route:list --path=products
Request matching flow
flowchart LR
A[HTTP method and path] --> B[Route table match]
B --> C[Closure or controller action]
C --> D[Response]
In the next lesson, you will build on this by adding route parameters and constraints so each route can target specific resources safely.
From Laravel best practices — Routing, Controllers & Request Flow