Lập trình · 22/09/2026

Laravel 13 Course – Lesson 05: Routing, Controllers and Middleware in Laravel 13

Lesson 5 — Routing, Controllers and Middleware: keep the existing TaskFlow page while separating URL selection, screen handling and request identification. Complete lesson 4 on configuration and logging first: this example retains config/taskflow.php and opt-in logging.

Routing, Controller và Middleware trong Laravel 13

Lesson 5 — Routing, Controllers and Middleware: keep the existing TaskFlow page while separating URL selection, screen handling and request identification. Complete lesson 4 on configuration and logging first: this example retains config/taskflow.php and opt-in logging.

1. Why separate the closure?

The previous closure was adequate for a small page. Reusing request identification across screens by copying code into each route would invite omissions. Our route will map GET /taskflow to a controller, the controller will prepare view data, and middleware will create an identifier and attach it to the response. Not every closure needs immediate conversion; this milestone is a deliberate exercise in responsibility boundaries.

GET /taskflow
  → AssignRequestId: generate UUID and set request attribute
  → TaskFlowOverviewController: read config, log, return view
  → AssignRequestId: attach X-Request-ID to response
  → browser receives HTML and header

A request attribute is server-side data, not user-supplied query input. X-Request-ID is a diagnostic correlation value, not an authentication token, authorization decision or proof of a trusted caller.

2. Create middleware

php artisan make:middleware AssignRequestId

Replace app/Http/Middleware/AssignRequestId.php with:

<?php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class AssignRequestId
{
    public function handle(Request $request, Closure $next): Response
    {
        $id = (string) Str::uuid();
        $request->attributes->set('request_id', $id);
        $response = $next($request);
        $response->headers->set('X-Request-ID', $id);

        return $response;
    }
}

Code before $next runs on the way in; code afterward runs when the inner layer returns a response. We generate a fresh server-side value rather than reflecting arbitrary client headers. Do not incorporate email, IP addresses or tokens. This diagnostic UUID is not yet a distributed trace propagated across services.

This middleware is attached only to /taskflow. It does not promise a header on every application 404: unmatched requests may never enter it. Universal request identification, including errors before routing, requires deliberate global middleware and exception-rendering design. Do not claim a broader guarantee than the implementation provides.

3. Create a single-action controller

php artisan make:controller TaskFlowOverviewController --invokable

Replace app/Http/Controllers/TaskFlowOverviewController.php:

<?php
namespace App\Http\Controllers;

use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class TaskFlowOverviewController extends Controller
{
    public function __invoke(Request $request): View
    {
        if (config('taskflow.overview_logging')) {
            Log::info('taskflow.overview.viewed', [
                'route' => 'taskflow.overview',
                'request_id' => $request->attributes->get('request_id'),
            ]);
        }

        return view('taskflow.overview', [
            'projectName' => config('taskflow.name'),
            'milestone' => 'Request lifecycle',
        ]);
    }
}

The __invoke method makes the controller a single action. Laravel supplies Request through its type hint; do not construct a request yourself or read $_GET directly. The controller returns a View with the previous template and data. Its allowlisted log context now contains the same identifier returned in the response header.

4. Connect the route

At this milestone, routes/web.php contains:

<?php
use App\Http\Controllers\TaskFlowOverviewController;
use App\Http\Middleware\AssignRequestId;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/taskflow', TaskFlowOverviewController::class)
    ->middleware(AssignRequestId::class)
    ->name('taskflow.overview');

Using the middleware class directly requires no alias in bootstrap/app.php. A route group can share middleware later. Avoid registering it globally and on the same route unless duplicate execution is intended. The taskflow.overview route name stays stable, so route()-based links need no rename.

php artisan route:list --path=taskflow -vv
php artisan test

Expect GET|HEAD, TaskFlowOverviewController and AssignRequestId in the route output. Replace the old route instead of adding another GET definition for the same URI. For future resource routes, verify interactions between fixed paths such as /tasks/create and parameters such as /tasks/{task}; constraints and ordering deserve tests.

5. Test identification and update log expectations

Create tests/Feature/TaskFlowRequestIdTest.php:

<?php
namespace Tests\Feature;

use Illuminate\Support\Str;
use Tests\TestCase;

class TaskFlowRequestIdTest extends TestCase
{
    public function test_request_id_is_server_generated_and_changes_per_request(): void
    {
        $first = $this->withHeader('X-Request-ID', 'untrusted-client-value')
            ->get('/taskflow')->assertOk();
        $id = $first->headers->get('X-Request-ID');
        $this->assertTrue(Str::isUuid($id));
        $this->assertNotSame('untrusted-client-value', $id);

        $second = $this->get('/taskflow')->assertOk();
        $this->assertNotSame($id, $second->headers->get('X-Request-ID'));
    }
}

The lesson 4 log test expects only a route field. Update that expectation deliberately rather than deleting the test. Assign the get() result to $response and use this context:

['route' => 'taskflow.overview',
 'request_id' => $response->headers->get('X-Request-ID')]

This checks agreement between the logged ID and response header. The new test checks server generation, UUID shape and different values across two requests. The current suite passed 8 tests with 18 assertions. Two distinct UUIDs are a regression check, not mathematical proof that collisions can never occur.

6. Observe a real HTTP response

Run the local server from lesson 2 and invoke curl; PowerShell users may need curl.exe:

curl -i http://127.0.0.1:8000/taskflow

Find X-Request-ID in the response. If TASKFLOW_OVERVIEW_LOGGING is enabled and configuration is not stale, find that ID in the selected log channel. Logs need not become public: an operator can receive the identifier with an error report and investigate internally.

7. Exercise and troubleshooting

Temporarily remove the middleware and predict which tests fail, then restore it. Change the path to /workspace while preserving the route name and discover remaining hardcoded URLs. Never use the request ID for authorization; TaskFlow access control comes in the authentication and policy section.

For missing classes, compare namespaces, paths and imports. For missing headers, inspect route:list before editing Blade. For absent logs, revisit the configuration flag and level from lesson 4. No database query is required here, and this lesson does not establish a complete task-management feature.

References: Routing, Controllers, Middleware. Navigation: Lesson 4 · Roadmap. Next we organize the interface with Blade components and Vite.

Laravel 13 course navigation

Previous lesson (04) · Next lesson (06) · All 37 lessons

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.