VS Vikesh Singh

Courses PHP & Laravel Lesson 7

PHP & LARAVEL · LESSON 7 OF 18

Routes and Controllers: How a Laravel Request Flows

· 5 min read

Lesson 6 introduced the idea that Laravel maps URLs to controller code through routing.

Lesson 6 introduced the idea that Laravel maps URLs to controller code through routing. This lesson goes deeper into exactly how that works, because understanding the full request flow is what lets you debug a real application instead of guessing when something doesn't behave the way you expect.

Defining Routes

Every Laravel request starts in routes/web.php (for normal browser-facing pages) or routes/api.php (for JSON API endpoints, typically stateless and without session/CSRF handling). These files are just PHP, read top to bottom when the application boots, registering a map of URL patterns to handlers:

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

Route::get('/trades', [TradeController::class, 'index'])->name('trades.index');
Route::get('/trades/{trade}', [TradeController::class, 'show'])->name('trades.show');
Route::post('/trades', [TradeController::class, 'store'])->name('trades.store');

Each line declares an HTTP verb (GET, POST, and so on), a URL pattern, and the controller method that should handle it. The {trade} segment is a route parameter, a placeholder that captures whatever value appears in that position of the URL. ->name('trades.show') attaches a name to the route that has nothing to do with the URL text itself; it is an internal identifier you use in your own code to generate a link to this route without hardcoding the URL string anywhere (route('trades.show', $trade) generates the correct URL even if you later change the actual path in this file, which is the whole point).

Notice something important: GET /trades and POST /trades are two entirely separate routes, even though the URL text is identical, because the HTTP verb is part of what Laravel matches on. This is deliberate and matches how the web is supposed to work: GET requests should only read data and never change anything (a user should be able to refresh a GET page safely, or a search engine should be able to crawl it, with zero side effects), while POST (and PUT, PATCH, DELETE) requests are the ones allowed to create, update, or delete something. Violating this convention, say, deleting a record on a GET request because it was "easier," causes real problems: a user hitting refresh, a browser prefetching links, or a search engine crawler following a link can all accidentally trigger it.

Controllers And Their Methods

Controllers are where the actual work happens. A controller method receives the route parameters as method arguments, and Laravel's "route model binding" feature will even resolve a database model for you automatically if you type-hint it correctly:

namespace App\Http\Controllers;

use App\Models\Trade;
use Illuminate\Http\Request;

class TradeController extends Controller
{
    public function index()
    {
        $trades = Trade::where('user_id', auth()->id())->latest()->get();

        return view('trades.index', ['trades' => $trades]);
    }

    public function show(Trade $trade)
    {
        return view('trades.show', ['trade' => $trade]);
    }

    public function store(Request $request)
    {
        $trade = Trade::create($request->only(['symbol', 'quantity', 'entry_price']));

        return redirect()->route('trades.show', $trade);
    }
}

Route Model Binding

Look closely at show(Trade $trade). The route declared {trade} as a plain URL segment, something like the number 42. But the controller method receives a fully loaded Trade model object, not the raw number 42. This is route model binding: because the parameter is type-hinted as Trade, Laravel automatically runs Trade::findOrFail(42) behind the scenes before your method body even runs, and if no trade with that ID exists, Laravel automatically returns a 404 response without you writing a single line of lookup or error-handling code. This is an excellent example of Laravel's general philosophy from lesson 6: a problem every single controller would otherwise solve slightly differently (look up the record, handle the not-found case) is instead solved once, consistently, by the framework.

index() demonstrates where a controller should (and should not) put logic. Trade::where('user_id', auth()->id())->latest()->get() is an Eloquent query (full detail in lesson 9); the key design point here is that the controller's job is to coordinate: fetch the data it needs, and hand it to a view. It is not the controller's job to contain complex business logic, like computing portfolio statistics across hundreds of trades with multiple conditional rules. That kind of logic belongs in a dedicated class (often called a "service" or placed directly on the model), specifically so it can be tested and reused without needing an HTTP request to invoke it. A controller method that is forty lines long, mixing database queries, business rules, and formatting, is a sign the logic needs to move somewhere else; this is one of the most common code-review complaints in real Laravel teams.

view('trades.index', [...]) renders a Blade template (lesson 8 covers this fully) and passes data to it; the dot in 'trades.index' maps to the file resources/views/trades/index.blade.php, another example of Laravel's folder conventions from lesson 6. redirect()->route(...) after a successful store() is the standard pattern after a POST request: never render a view directly as the response to a form submission, because doing so means hitting the browser's refresh button resubmits the form (triggering the action a second time, which for something like a payment or a trade entry would be a real bug). Redirecting after a successful write, so the final page the browser lands on came from a safe GET request, avoids that entirely; this is a well-known pattern called "redirect after POST," and it is not Laravel-specific, but Laravel's store() convention makes it the natural default.

Terminology Wrap-up

A quick but important terminology note for later lessons: everything described here, routing, controllers, and the request/response objects themselves, lives inside what Laravel calls the HTTP layer, found under app/Http/. Middleware (lesson 13) runs inside this same layer, positioned specifically between the raw incoming request and the controller method actually being called.

Try it yourself: add a Route::delete('/trades/{trade}', [TradeController::class, 'destroy'])->name('trades.destroy'); line, write the matching destroy(Trade $trade) method using route model binding, and have it call $trade->delete() followed by a redirect back to trades.index.