Originally published on laravel.learnio.dev — part of the Laravel best practices course. View this lesson on the original site.

Build a complete product routing surface

This exercise combines chapter 2 skills into one coherent route/controller design.

Goal

Create a CRUD route surface for products with:

  • clear HTTP verbs,
  • named routes,
  • controller actions,
  • parameter constraints,
  • route model binding on show/update/delete paths.

Step 1: Create a controller

php artisan make:controller ProductController

Add placeholder methods: index, store, show, update, destroy.

Step 2: Define CRUD routes in routes/web.php

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

Route::prefix('products')->name('products.')->group(function () {
    Route::get('/', [ProductController::class, 'index'])->name('index');
    Route::post('/', [ProductController::class, 'store'])->name('store');
    Route::get('/{product}', [ProductController::class, 'show'])
        ->whereNumber('product')
        ->name('show');
    Route::patch('/{product}', [ProductController::class, 'update'])
        ->whereNumber('product')
        ->name('update');
    Route::delete('/{product}', [ProductController::class, 'destroy'])
        ->whereNumber('product')
        ->name('destroy');
});

Step 3: Use route model binding in controller methods

use App\Models\Product;

public function show(Product $product)
{
    return response()->json(['data' => $product]);
}

Apply similar signatures for update and destroy.

Step 4: Verify your route table

php artisan route:list --path=products -v

Check:

  • five routes exist,
  • names use products.*,
  • methods are correct,
  • middleware stack is what you expect.

Step 5: Test the routes with sample requests

Run your app and send these example requests. Confirm the response status and shape match the expected result.

# list products
curl -i http://127.0.0.1:8000/products

# create product
curl -i -X POST http://127.0.0.1:8000/products \
  -H "Content-Type: application/json" \
  -d '{"name":"Demo Product","price":19.99}'

# show product (replace 1 with an existing id)
curl -i http://127.0.0.1:8000/products/1

# update product
curl -i -X PATCH http://127.0.0.1:8000/products/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Updated Product"}'

# delete product
curl -i -X DELETE http://127.0.0.1:8000/products/1

# invalid id should fail constraint or binding
curl -i http://127.0.0.1:8000/products/not-a-number

Expected checks:

  • GET /products returns success (200) and your list payload,
  • POST /products returns success (200/201 depending on your implementation),
  • GET /products/1 returns success (200) when id exists,
  • PATCH /products/1 returns success (200) and updated data,
  • DELETE /products/1 returns success (200/204) in your implementation,
  • GET /products/not-a-number returns 404.

If any response does not match expectations, update routes, constraints, or controller signatures and rerun the same request until results are consistent.

Bad way vs better way

Bad way (mixed intent in one any route):

Route::any('/products/{product?}', [ProductController::class, 'handle']);

Better way (explicit CRUD contract):

Route::get('/products', [ProductController::class, 'index'])->name('products.index');
Route::post('/products', [ProductController::class, 'store'])->name('products.store');
Route::patch('/products/{product}', [ProductController::class, 'update'])->name('products.update');

Why better:

  • clearer endpoint intent,
  • easier policy and validation boundaries,
  • simpler automated testing.

Done criteria

  • route names and methods are consistent,
  • model binding is used where needed,
  • no duplicated route logic in controllers,
  • route list output matches your expected surface,
  • sample requests return expected statuses and shapes.

Exercise flow

flowchart LR
  A[Define CRUD routes] --> B[Wire controller actions]
  B --> C[Apply constraints and model binding]
  C --> D[Verify with route list]

After this exercise, you are ready for the chapter quiz with a production-like routing mental model.

From Laravel best practices — Routing, Controllers & Request Flow