The request lifecycle and the skeleton of Laravel 13
This chapter follows how Laravel processes a single request.
Middleware, the exception handler, and service providers all turn up in later chapters. Grasping where each of them plugs into the path first means you know where a new concept goes when it appears. Without this map, learning individual features leaves you guessing every time about which file the configuration goes in.
The path one request takes
Here is what happens from the moment GET /api/v1/products arrives from the browser until a response goes back.
There is only one entry point
Every request to a Laravel application goes through public/index.php1. The Nginx or Apache configuration points access to any URL at that file.
There is no file on the server that corresponds to the path /api/v1/products. Paths that do not exist are all gathered into index.php too, and from there PHP code decides how to handle them. This arrangement is called a front controller.
index.php itself is a short file. It does two things: load the autoloader Composer generated, and receive the application instance from bootstrap/app.php.
The first thing created is the service container
The first thing Laravel does is create the application — that is, an instance of the service container1.
The service container is the mechanism that manages classes and their dependencies. Ask for "an OrderPlacementService" and it assembles the classes its constructor needs as well, then hands the whole thing back. Chapter 6 covers this in detail. For now, think of it as a parts registry created at the very start of request processing.
The HTTP kernel takes charge of everything
Next the request is handed to the HTTP kernel. The kernel is an instance of Illuminate\Foundation\Http\Kernel, with a handle method that takes a Request and returns a Response1.
The official documentation describes the kernel as a large black box representing the whole application: put an HTTP request in, an HTTP response comes out.
Before it starts processing, the kernel runs a series of steps called bootstrappers. Configuring error handling, configuring logging, and detecting the environment all happen here1. These are Laravel internals and you do not normally need to think about them.
Service providers put the parts in place
After the bootstrappers, service providers are processed. The order here has a reason, so read carefully.
Laravel walks the list of providers and first creates instances of all of them. Then it calls the register method on every provider. Only once that is finished does it call the boot method on every provider1.
Finishing every register before moving on to boot guarantees that by the time boot runs, every container registration is complete. If provider A's boot wants to use a part registered by provider B, then without this ordering B might not be registered yet.
A rule about how to write providers follows from that ordering.
public function register(): void
{
// Just registers the mapping "when this type is asked for, return this class"
$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
}
public function register(): void
{
// Other providers may not have finished registering yet
Event::listen(OrderPlaced::class, SendOrderConfirmation::class);
}
The official documentation states plainly that the register method should only bind things into the service container, and that you should never attempt to register event listeners, routes, or any other piece of functionality within it2. Those go in boot.
Note that many of the providers Laravel uses internally are deferred providers. They are not loaded on every request, only when the service is actually needed2. Mail, queues, and the cache are all provided through providers, but they cost nothing on requests that do not use them.
Middleware, the router, and the controller
With everything now prepared, the Request begins passing through middleware. Middleware is a layer that touches both the request and the response; authentication, rate limiting, and CORS live here. How to write your own is covered in chapter 13.
A request that gets through the middleware reaches the router, which looks at the URL and method and hands it to the matching controller action. The response travels back out the way it came.
bootstrap/app.php is the entry point for configuration
Create a fresh project in Laravel 13 and bootstrap/app.php looks like this.
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
})->create();
This file, under 40 lines, is the entry point for the entire application's configuration. Each method corresponds to a later chapter.
| Method | Role | Chapter |
|---|---|---|
withRouting | Registering route files and the health check URL | Chapter 4 |
withMiddleware | Adding, removing, and grouping middleware | Chapter 13 |
withExceptions | Rules for converting exceptions into HTTP responses | Chapter 9 |
Three things are worth noticing now; they make later chapters easier.
There is no api: entry. The initial route files are just web.php and console.php3. The API's routes/api.php is created only when you run php artisan install:api, which also adds api: to this withRouting. We actually run it in chapter 4.
health: '/up' is there from the start. Hitting /up tells you whether the application can boot. Chapter 21 uses it as the target for a monitoring service.
withExceptions already has shouldRenderJsonWhen. It configures exceptions to come back as JSON rather than an HTML error page when the request path matches api/* or the client expects JSON. It is an important default when building an API, and chapter 9 continues from there.
A map of the directories
The app directory of a fresh project has only three things: Http, Models, and Providers3.
Jobs, Events, Policies, and Notifications do not exist at first. They are created when you run an Artisan command such as make:job or make:policy. Empty directories are not laid out in advance so that features you are not using stay out of sight.
Here are the directories this series uses.
| Location | Contents | Present initially | Chapter |
|---|---|---|---|
app/Http | Controllers, middleware, form requests | Yes | 4, 5, 13 |
app/Models | Eloquent models | Yes | 3 |
app/Providers | Service providers (one AppServiceProvider) | Yes | 6, 13 |
app/Exceptions | Your own exception classes (created by make:exception) | No | 9 |
app/Jobs | Jobs to put on a queue (created by make:job) | No | 16, 17 |
app/Policies | Authorization policy classes (created by make:policy) | No | 12 |
app/Notifications | Notification classes (created by make:notification) | No | 15 |
routes | web.php and console.php; api.php added by install:api | 2 files | 4, 20 |
database | Migrations, factories, seeders | Yes | 3 |
config | Configuration files | Yes | This chapter |
bootstrap | app.php and the framework cache | Yes | This chapter |
tests | Ships with sample Pest / PHPUnit tests | Yes | 10 |
The difference between routes/web.php and routes/api.php matters in this series. Routes in web.php are assigned the web middleware group, which brings sessions, CSRF protection, and cookie encryption. Routes in api.php, by contrast, are stateless: they assume token authentication and cannot access session state3. Routes in api.php also get an automatic /api prefix4.
This series uses only api.php.
The division of roles between config and .env
Configuration values are handled in two stages.
.env config/*.php Application code
(per-environment) → (interpretation → read with config('...')
and defaults)
.env holds values that differ per environment: the database connection, mail server credentials, the application's operating mode. Because the contents differ per environment, the file does not go in the repository.
The PHP files under config/ read the .env values and give them structure.
'debug' => (bool) env('APP_DEBUG', false),
The second argument to env() is the default when the environment variable is not set. Here, APP_DEBUG unset means false.
And from application code you read with config(), not env().
if (config('app.debug')) {
// ...
}
if (env('APP_DEBUG')) {
// null in production (the next section explains why)
}
This distinction is not a matter of taste. As the next section explains, env() stops working as expected in production.
What matters in production
Running config:cache stops .env from being read
In production you run php artisan config:cache, which merges every configuration file into one. Fewer files to load means faster boot.
Once you run that command, the .env file is no longer loaded, for either request processing or Artisan commands5. Calling env() returns a server-level or system-level environment variable, or otherwise null.
That is why env() is called only inside files under config/. Code that breaks this rule fails in the hardest way to find: it works locally and picks up null only in production. Chapter 21's deployment procedure returns to this.
Note that you do not run config:cache in local development, because you would have to rebuild the cache every time you change a setting5. If you run it by mistake, php artisan config:clear removes it.
Do not put .env in the repository
.env holds database passwords and API keys for external services. Laravel's .gitignore includes .env from the start, but it is still possible to make a copy under another name, such as .env.example, and commit that by accident.
If you have already committed it, the value stays in the history, so deleting it does not invalidate it. Reissue the value itself.
Do not set APP_DEBUG to true in production
When an exception occurs with APP_DEBUG=true, configuration values are displayed on screen along with the stack trace. On an API they are included in the JSON response. Always set it to false in production.
Reading older articles
Laravel has reorganized where configuration lives, so slightly older articles do not match the current structure. If an article you find in a search says something different from what you see, translate it as follows.
| What the older article says | How it is written now | Chapter in this series |
|---|---|---|
Register middleware in app/Http/Kernel.php | withMiddleware() in bootstrap/app.php | Chapter 13 |
Handle exceptions in app/Exceptions/Handler.php | withExceptions() in bootstrap/app.php | Chapter 9 |
Add providers to the providers array in config/app.php | Add them to bootstrap/providers.php | Chapter 6 |
Register events in the $listen property of EventServiceProvider | Automatic discovery by naming convention | Chapter 15 |
What goes in app/Http today is controllers, middleware, and form requests; there is no Kernel.php3. app/Exceptions is where your own exception classes go, and it does not exist initially — make:exception creates it.
Every user-defined service provider is registered in bootstrap/providers.php2.
<?php
return [
App\Providers\AppServiceProvider::class,
];
app/Http/Kernel.php disappearing means the application side no longer holds a class extending the kernel. As the earlier section showed, Illuminate\Foundation\Http\Kernel is still at the center of request processing. Articles that say "the Kernel was removed" are blurring that distinction.
Summary
- Every request goes through
public/index.php. It is the one and only entry point - The first thing Laravel creates is the service container. Everything after that sits on top of it
- Service providers finish every
registerbefore moving on toboot. That is whyregisterholds only container bindings bootstrap/app.phpis the entry point for routing, middleware, and exception handling configuration. The initial state has noapi:;install:apiadds it- Call
env()only inside files underconfig/. In production,config:cachestops.envfrom being read
What to read next
The next chapter, Modeling the subject and seeding data, builds the tables this series uses across 20 chapters. We define products, stock, orders, and order lines with migrations, map Eloquent models to enums, and set up development data with factories and seeders. The up-front investment here pays off in every chapter that follows.
Exercises
The following code behaves as intended locally but does not work in production. Explain why
class ExternalApiClient
{
public function __construct()
{
$this->apiKey = env('PAYMENT_API_KEY');
$this->timeout = env('PAYMENT_TIMEOUT', 30);
}
}
Sample answer
Production runs php artisan config:cache, so the .env file is no longer loaded. As a result env('PAYMENT_API_KEY') returns null and the payment API call fails authentication.
The nasty part is that env('PAYMENT_TIMEOUT', 30) returns its default of 30, so it keeps working without an error. You never notice the setting is not taking effect.
The fix is to move the values into a configuration file such as config/services.php and read them from code with config().
// config/services.php
return [
'payment' => [
'key' => env('PAYMENT_API_KEY'),
'timeout' => env('PAYMENT_TIMEOUT', 30),
],
];
public function __construct()
{
$this->apiKey = config('services.payment.key');
$this->timeout = config('services.payment.timeout');
}
config:cache reads everything under config/ to build the cache, so in this form the values survive in production.
Why must you not register event listeners in a service provider's register method?
Sample answer
Laravel finishes calling register on every provider before moving on to calling boot. In other words, at the time register runs, other providers may not have finished registering.
Registering an event listener assumes the event dispatcher is already registered in the container. The place where that assumption is guaranteed to hold is boot.
The official documentation states plainly that register should only bind things into the service container. A binding just writes "when asked for by this name, return this class" into the registry, and needs no other parts. That is why it is unaffected by ordering.