A deep dive into the Slim Application Skeleton introduced in Laravel 11, covering everything from Application::configure() to ApplicationBuilder internals.
Laravel 11 introduced the “Slim Application Skeleton,” a significant overhaul of the application structure. The biggest change is the consolidation of scattered configuration into a single place: bootstrap/app.php.
Item
Laravel 10 and earlier
Laravel 11 and later
HTTP kernel
app/Http/Kernel.php
Removed (merged into framework)
Console kernel
app/Console/Kernel.php
Removed (moved to routes/console.php)
Exception handler
app/Exceptions/Handler.php
Removed (consolidated into bootstrap/app.php)
Service providers
5 files
Single AppServiceProvider.php
Route files
web.php / api.php by default
Only web.php by default; api.php is opt-in
Bootstrap
Configuration spread across multiple files
Consolidated into bootstrap/app.php
Default DB
MySQL/PostgreSQL
SQLite
This change is intended for new projects. Upgrading an existing application from an older version leaves the previous structure fully functional.
This single file configures routing, middleware, and exception handling. In Laravel 10 and earlier, these settings were spread across app/Http/Kernel.php, app/Console/Kernel.php, and app/Exceptions/Handler.php.
This file is the registration point for service providers. In Laravel 10, providers were listed in the providers array inside config/app.php. They have now been separated into bootstrap/providers.php. Only AppServiceProvider is included by default.
When you install a package via composer require, it may automatically update bootstrap/providers.php. While config/app.php is still referenced, bootstrap/providers.php is the recommended location for new registrations.
The HTTP kernel was merged into the framework’s Illuminate\Foundation\Http\Kernel. Middleware customization is now done via withMiddleware() in bootstrap/app.php.
The two responsibilities of the console kernel were separated. Artisan commands placed in app/Console/Commands/ are auto-discovered, and schedules are defined in routes/console.php.
// Laravel 10 and earlier: app/Console/Kernel.phpprotected function schedule(Schedule $schedule): void{ $schedule->command('emails:send')->daily();}
// Laravel 11 and later: routes/console.phpuse Illuminate\Support\Facades\Schedule;Schedule::command('emails:send')->daily();
Removal of app/Exceptions/Handler.php
The exception handler was merged into the framework’s Illuminate\Foundation\Exceptions\Handler. Customization is now done via withExceptions() in bootstrap/app.php.
// Laravel 10 and earlier: app/Exceptions/Handler.phppublic function register(): void{ $this->reportable(function (InvalidOrderException $e) { // ... });}
Application::configure() is a static method on Illuminate\Foundation\Application.
// From Illuminate\Foundation\Applicationpublic static function configure(?string $basePath = null){ $basePath = match (true) { is_string($basePath) => $basePath, default => static::inferBasePath(), }; return (new Configuration\ApplicationBuilder(new static($basePath))) ->withKernels() ->withEvents() ->withCommands() ->withProviders();}
This method does the following:
Determines the application root directory from basePath
Creates an Application instance
Wraps it in an ApplicationBuilder and applies default configuration
Returns the ApplicationBuilder instance
The important detail is that withKernels(), withEvents(), withCommands(), and withProviders() are already called inside configure(). You do not need to call them again in bootstrap/app.php.
public/index.php is the entry point. It loads bootstrap/app.php to obtain the Application, after which the HTTP Kernel passes the request through the middleware pipeline and the router dispatches it to a controller.
Internally, it registers a callback with AppRouteServiceProvider::loadRoutesUsing() and registers AppRouteServiceProvider during the application’s boot phase.
// Simplified internal processing of withRouting()protected function buildRoutingCallback(...){ return function () use ($web, $api, $pages, $health, $apiPrefix, $then) { if (is_string($api) || is_array($api)) { Route::middleware('api')->prefix($apiPrefix)->group($api); } if (is_string($health)) { Route::get($health, function () { Event::dispatch(new DiagnosingHealth); return response(View::file(...), status: 200); }); } if (is_string($web) || is_array($web)) { Route::middleware('web')->group($web); } if (is_callable($then)) { $then($this->app); } };}
Key points:
api routes automatically get the api middleware group and the /api prefix
Passing a string to health auto-registers a health-check endpoint (default /up)
The health path is excluded during maintenance mode (configured via PreventRequestsDuringMaintenance::except())
api is registered before web — if you define routes with the same path for both, the API route takes precedence
Passing a string to pages enables Laravel Folio routing
public function withMiddleware(?callable $callback = null){ $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) { $middleware = (new Middleware) ->redirectGuestsTo(fn () => route('login')); if (! is_null($callback)) { $callback($middleware); } $kernel->setGlobalMiddleware($middleware->getGlobalMiddleware()); $kernel->setMiddlewareGroups($middleware->getMiddlewareGroups()); $kernel->setMiddlewareAliases($middleware->getMiddlewareAliases()); // ... }); return $this;}
withMiddleware() executes the callback afterHttpKernel is resolved, using an afterResolving() hook. The Middleware object passed to the callback provides a rich set of customization methods.
->withMiddleware(function (Middleware $middleware) { // Add a global middleware $middleware->append(MyGlobalMiddleware::class); // Append middleware to the web group $middleware->web(append: [EnsureUserIsSubscribed::class]); // Replace middleware in the api group $middleware->api(replace: [ OldMiddleware::class => NewMiddleware::class, ]); // Set CSRF exclusion paths $middleware->validateCsrfTokens(except: ['stripe/*', 'webhook/*']); // Change the redirect destination for unauthenticated users $middleware->redirectGuestsTo('/custom-login'); // Set middleware priority $middleware->priority([ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, ]);})
public function withExceptions(?callable $using = null){ $this->app->singleton( \Illuminate\Contracts\Debug\ExceptionHandler::class, \Illuminate\Foundation\Exceptions\Handler::class ); if ($using !== null) { $this->app->afterResolving( \Illuminate\Foundation\Exceptions\Handler::class, fn ($handler) => $using(new Exceptions($handler)), ); } return $this;}
withExceptions() registers the framework’s Handler class as a singleton and then sets up the callback via afterResolving(). The callback receives an Exceptions wrapper object.
->withExceptions(function (Exceptions $exceptions) { // Do not report a specific exception $exceptions->dontReport(MissedFlightException::class); // Custom reporting for a specific exception $exceptions->report(function (InvalidOrderException $e) { // e.g., send a Slack notification }); // Customize the HTTP response for a specific exception $exceptions->render(function (NotFoundHttpException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => 'Not Found'], 404); } }); // Throttle repeated reports of the same exception $exceptions->throttle(function (Throwable $e) { return Limit::perMinute(20); });})
public function withProviders(array $providers = [], bool $withBootstrapProviders = true){ RegisterProviders::merge( $providers, $withBootstrapProviders ? $this->app->getBootstrapProvidersPath() : null ); return $this;}
Because withProviders() is called by default inside Application::configure(), bootstrap/providers.php is loaded automatically. If you need to register additional providers, call withProviders() explicitly in bootstrap/app.php.
Application::configure(basePath: dirname(__DIR__)) ->withProviders([ // Additional providers on top of the default bootstrap/providers.php App\Providers\CustomServiceProvider::class, ]) ->withRouting(...) ->create();
Passing withBootstrapProviders: false prevents bootstrap/providers.php from being loaded. Omit this option unless you have a specific reason to do so.
In Laravel 10 and earlier, app/Http/Kernel.php used arrays to enumerate middleware, resembling a configuration file. This approach provided limited benefit from PHP’s type system and IDE support.Laravel 11 switches to the callback style withMiddleware(function (Middleware $middleware) { ... }). This enables type completion and makes dynamic configuration using conditionals and loops feel natural.
From “convention over configuration” to “explicit configuration”
Making api.php opt-in removes the overhead of always loading the api middleware group for applications that do not use API routes. The philosophy is: if you do not use a feature, it should not exist by default.
withMiddleware() and withExceptions() use afterResolving() to avoid ordering issues. The ApplicationBuilder methods are called before the application is fully bootstrapped, but the actual work (applying configuration to the kernel) is deferred until the kernel is first resolved.
->withMiddleware(function (Middleware $middleware) { // Add an authentication-check middleware to web routes $middleware->web(append: [ \App\Http\Middleware\EnsureEmailIsVerified::class, ]); // Remove a specific middleware from API routes $middleware->api(remove: [ \Illuminate\Session\Middleware\StartSession::class, ]); // Exclude webhook endpoints from CSRF protection $middleware->validateCsrfTokens(except: [ 'webhook/*', 'stripe/webhook', ]); // Assign an alias to a specific middleware $middleware->alias([ 'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class, ]);})
ApplicationBuilder’s registered(), booting(), and booted() methods simply register callbacks on the Application instance. You rarely need them in normal usage, but they enable advanced scenarios — for example, modifying a fully-booted Kernel via booted():