How a request becomes a response
The journey from `public/index.php` to the bytes a browser receives β and the one object that makes it all fit together.
Every Laravel request follows the same path. Knowing it turns "where do I put this?" into a question with an obvious answer.
public/index.php
β
βΌ
bootstrap/app.php βββΊ the Application (the service container)
β
βΌ
HTTP kernel
β
βββΊ global middleware (trim strings, handle CORSβ¦)
β
βΌ
Router βββΊ matches a route
β
βββΊ route middleware (auth, throttle, your own)
β
βΌ
Controller / closure βββΊ returns a value
β
βΌ
Response βββΊ middleware again, outward this time
β
βΌ
bytes on the wireOne object at the centre
bootstrap/app.php builds an Application β a service container that knows how to construct everything else. Every part of the framework is resolved from it.
<?php
echo 'Laravel ', app()->version(), PHP_EOL;
echo 'environment: ', app()->environment(), PHP_EOL;
echo 'router: ', get_class(app('router')), PHP_EOL;
echo 'config repo: ', get_class(app('config')), PHP_EOL;
echo 'running in console: ', var_export(app()->runningInConsole(), true), PHP_EOL;The app() helper is the container. Everything else in this chapter is a thing the container hands out.
Routes are registered, then matched
Route registration builds a table. Nothing is executed until a request arrives and the router finds a match.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/hello', fn () => 'Hello!');
Route::post('/hello', fn () => 'Posted!');
echo visit('/hello')->getContent(), PHP_EOL;
echo visit('/missing')->getStatusCode(), PHP_EOL; // 404 β no route matchedWhatever you return becomes a response
The framework converts your return value. A string becomes a 200 text/html; an array or a JsonResource becomes JSON; a Response object is used as-is.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/string', fn () => 'plain text');
Route::get('/array', fn () => ['name' => 'Ada', 'age' => 36]);
Route::get('/custom', fn () => response('made by hand', 201)->header('X-Course', 'ThongLearn'));
$r = visit('/string');
echo $r->getStatusCode(), ' ', $r->headers->get('content-type'), PHP_EOL;
$j = visit('/array');
echo $j->headers->get('content-type'), ' β ', $j->getContent(), PHP_EOL;
$c = visit('/custom');
echo $c->getStatusCode(), ' ', $c->headers->get('X-Course'), ' β ', $c->getContent(), PHP_EOL;Middleware wraps the whole thing
Middleware is an onion. Each layer sees the request on the way in and the response on the way out.
<?php
use Illuminate\Support\Facades\Route;
class Stopwatch
{
public function handle($request, \Closure $next)
{
// before: on the way in
$response = $next($request);
// after: on the way out
return $response->header('X-Handled', 'yes');
}
}
Route::get('/timed', fn () => 'work done')->middleware(Stopwatch::class);
$r = visit('/timed');
echo $r->getContent(), ' | X-Handled: ', $r->headers->get('X-Handled'), PHP_EOL;π§ Scenario: You need to log how long each request took. A before hook cannot know the duration and an after hook cannot know the start β but a middleware sees both sides of $next($request), so it is the only place that can. That "both sides" property is why so much of Laravel lives in middleware.
Errors become responses too
An uncaught exception does not kill the request. The exception handler converts it β to a 500, or to whatever the exception says it is.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/missing-model', fn () => abort(404, 'No such thing'));
Route::get('/not-allowed', fn () => abort(403, 'Nope'));
Route::get('/gone', fn () => abort(410));
foreach (['/missing-model', '/not-allowed', '/gone'] as $uri) {
echo $uri, ' β ', visit($uri)->getStatusCode(), PHP_EOL;
}Exceptions implementing HttpExceptionInterface β which is what abort() throws β carry their own status code. Everything else becomes a 500 and is written to the log with a full stack trace.
Then the process ends
Like any PHP application, Laravel is shared-nothing: once the response is sent, the container, every service in it and every loaded model are discarded. The next request builds them all again.
<?php
use Illuminate\Support\Facades\Route;
Route::get('/counter', function () {
static $count = 0; // survives within ONE process...
return 'count: ' . ++$count;
});
echo visit('/counter')->getContent(), PHP_EOL;
echo visit('/counter')->getContent(), PHP_EOL;
echo 'β¦but a real request boundary resets everything', PHP_EOL;π‘ Tip: This is why "just cache it in a static property" is not a caching strategy. Anything that must outlive a request goes in the cache, the session or the database. The exception is a long-running worker β Octane, queues β where the container does persist, and where static state becomes a genuine source of cross-request bugs.
In this course
Your code runs inside a real Laravel 13 application in your browser, with an in-memory SQLite database. visit('/uri') sends a request through the full stack above and hands you the Response.
Reference: Request Lifecycle in the Laravel documentation.