Directory Structure and Configuration

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05. Every listing and output comes from a new application created with laravel new. Laravel 12 has the same structure; applications created with Laravel 10 or older and upgraded since may still use the older structure described at the end.

The directory structure

A new Laravel 13 application has few directories. Laravel 11 introduced this slim skeleton: directories that an application may never need are created only when an Artisan generator first writes into them.

Directory Contents

app/

The application code, namespace App\. A new application has only Http/Controllers, Models (with User) and Providers (with AppServiceProvider)

bootstrap/

app.php, which configures and creates the application, providers.php, the list of service providers, and cache/ for the framework’s cached files

config/

One PHP file per subsystem (app.php, auth.php, cache.php, database.php, filesystems.php, logging.php, mail.php, queue.php, services.php, session.php); others are published on demand

database/

migrations/, factories/, seeders/ and the default database.sqlite

public/

The web server’s document root: index.php (the front controller) and built assets

resources/

Blade views, uncompiled CSS and JavaScript, and (after lang:publish) translations

routes/

web.php for web routes and console.php for closure commands and the schedule; api.php and channels.php are added by install:api and install:broadcasting

storage/

Logs, compiled views, file sessions and caches, and uploaded files (app/public, linked to public/storage by storage:link)

tests/

Feature/ and Unit/ tests, with Pest.php and TestCase.php

vendor/

Composer packages

Generators create their directories as they go. In a new application:

$ ls -d app/*/
app/Http/  app/Models/  app/Providers/
$ php artisan make:job SendReviewDigest
 INFO Job [app/Jobs/SendReviewDigest.php] created successfully.
$ php artisan make:middleware EnsureUserIsEditor
 INFO Middleware [app/Http/Middleware/EnsureUserIsEditor.php] created successfully.
$ php artisan make:event ReviewPosted
 INFO Event [app/Events/ReviewPosted.php] created successfully.
$ php artisan make:policy ReviewPolicy
 INFO Policy [app/Policies/ReviewPolicy.php] created successfully.
$ php artisan make:request StoreReviewRequest
 INFO Request [app/Http/Requests/StoreReviewRequest.php] created successfully.
$ php artisan make:command ImportBooks
 INFO Console command [app/Console/Commands/ImportBooks.php] created successfully.
$ ls -d app/*/ app/Http/*/
app/Console/  app/Events/  app/Http/  app/Jobs/  app/Models/  app/Policies/  app/Providers/
app/Http/Controllers/  app/Http/Middleware/  app/Http/Requests/

Nothing forces this layout beyond Composer’s PSR-4 mapping of App\ to app/: large applications often group code by domain (app/Catalogue/, app/Reviews/). Laravel discovers event listeners, policies and commands by convention in the default directories, so moving them means registering them explicitly.

bootstrap/app.php

bootstrap/app.php configures the application with a fluent builder and returns it. It replaced the HTTP and console Kernel classes, the exception Handler and RouteServiceProvider that older applications had. The file in a new application:

<?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();
  • withRouting() names the route files (web, api, commands, channels), the health-check URL (/up, which returns 200 when the application boots, for load balancers), and options such as apiPrefix or a then: closure for extra route files.

  • withMiddleware() adds, removes, groups and aliases middleware and configures built-in ones (trusted proxies, CSRF exceptions, cookie encryption exceptions); see Middleware.

  • withExceptions() customises reporting and rendering; the skeleton renders JSON errors for api/* URLs and for requests that ask for JSON. See Error Handling and Logging.

  • withProviders(), withEvents(), withCommands(), withSchedule() and withBroadcasting() configure the rest when the defaults are not enough.

bootstrap/providers.php lists the application’s service providers; make:provider adds to it automatically:

<?php

use App\Providers\AppServiceProvider;

return [
    AppServiceProvider::class,
];

The bootstrap sequence

public/index.php is short: it checks for maintenance mode, loads Composer’s autoloader, requires bootstrap/app.php and hands the request to the application. Before routing, the HTTP kernel runs six bootstrappers, in this order (read from Illuminate\Foundation\Http\Kernel::$bootstrappers in 13.34):

sequenceDiagram participant I as public/index.php participant B as bootstrap/app.php participant A as Application participant K as HTTP Kernel I->>I: maintenance.php present? serve 503 I->>I: require vendor/autoload.php I->>B: require B->>A: Application::configure() withRouting / withMiddleware / withExceptions B-->>I: create() returns the application I->>A: handleRequest(Request::capture()) A->>K: handle(request) K->>K: LoadEnvironmentVariables (.env, unless config is cached) K->>K: LoadConfiguration (config/*.php or bootstrap/cache/config.php) K->>K: HandleExceptions (error and exception handlers) K->>K: RegisterFacades K->>K: RegisterProviders (register() of every provider) K->>K: BootProviders (boot() of every provider) K->>K: middleware pipeline, router, controller K-->>I: response sent, then terminable middleware

The rest of the path, from middleware to response, is on Request Lifecycle.

Configuration and the .env file

Configuration lives in config/*.php files that return arrays. Values that differ per environment (database host, debug mode, API keys) are read from environment variables with env(); in development they come from the .env file, loaded by phpdotenv, and in production usually from the real environment. Code reads configuration with config('file.key'):

<?php
// config/app.php (excerpt)
return [
    'name' => env('APP_NAME', 'Laravel'),
    'debug' => (bool) env('APP_DEBUG', false),
    'url' => env('APP_URL', 'http://localhost'),
];

// anywhere in the application
$name = config('app.name');
config(['app.locale' => 'es']);      // set at run time, for this request only
$ php artisan tinker --execute='var_dump(env("APP_DEBUG"), env("MISSING", "fallback"), config("app.debug"), config("database.default"), app()->environment("local", "staging"));'
bool(true)
string(8) "fallback"
bool(true)
string(6) "sqlite"
bool(true)

env() converts the strings true, false, empty and null (and their parenthesised forms) to PHP values, which is why APP_DEBUG=true arrived as bool(true); quote a value with spaces (APP_NAME="Book Shelf").

env() only in config files

php artisan config:cache merges all configuration files into bootstrap/cache/config.php. After that, the .env file is not loaded at all, so env() calls outside config/ return null. A route that reads both, before and after caching:

<?php
// routes/web.php
Route::get('/config-demo', fn () => [
    'config_app_name' => config('app.name'),
    'env_app_name' => env('APP_NAME'),             // only safe inside config/*.php
    'debug' => config('app.debug'),
    'environment' => app()->environment(),
    'is_local' => app()->isLocal(),
]);
$ curl localhost:8000/config-demo
{"config_app_name":"Laravel","env_app_name":"Laravel","debug":true,"environment":"local","is_local":true}
$ php artisan config:cache
 INFO Configuration cached successfully.
$ curl localhost:8000/config-demo
{"config_app_name":"Laravel","env_app_name":null,"debug":true,"environment":"local","is_local":true}
$ php artisan config:clear

Run config:cache (and route:cache, view:cache, event:cache, or all of them with php artisan optimize) when deploying, never in development, where configuration changes would be ignored.

Publishing more configuration

Only the commonly changed configuration files are in a new application. config:publish copies others from the framework (cors, view, broadcasting, hashing, concurrency, …), and config:publish --all copies them all:

$ php artisan config:publish cors
 INFO Published 'cors' configuration file.

Encrypted environment files

.env must not be committed. To keep environment files in version control anyway, php artisan env:encrypt writes .env.encrypted (AES-256-CBC by default) and prints the key, which belongs in a password manager or the deployment platform’s secrets; env:decrypt restores .env using the key from the LARAVEL_ENV_ENCRYPTION_KEY variable or --key. --env=staging encrypts .env.staging, and --readable keeps the variable names visible so that changes are reviewable.

Environments

APP_ENV names the environment (local, testing, staging, production). app()->environment() returns it, app()->environment('local', 'staging') checks it, and App::isProduction() / isLocal() are shortcuts. When APP_ENV (or the --env option of Artisan) is set, Laravel loads .env.{APP_ENV} if it exists, which is how tests use .env.testing. Prefer configuration values over environment checks in code: if (config('services.payments.live')) can be tested and changed per deployment, if (app()->isProduction()) cannot.

Debug mode

APP_DEBUG=true shows detailed error pages with the stack trace, the request, the configuration and environment variables. It is essential locally and dangerous in production, where it can expose database passwords and API keys to anyone who triggers an error. Set APP_DEBUG=false in production; php artisan about and the health route help confirm what a server is running with.

Maintenance mode

php artisan down makes every request return 503 Service Unavailable with a maintenance page, while Artisan commands keep working; queued jobs are not processed while the application is down. --refresh sends a Refresh header, and --secret defines a URL that sets a cookie letting you browse the site normally:

$ php artisan down --secret=preview-7f3a --refresh=15
 INFO Application is now in maintenance mode.
 INFO You may bypass maintenance mode via [http://localhost:8000/preview-7f3a].
$ curl -s -o /dev/null -D - localhost:8000/books
HTTP/1.1 503 Service Unavailable
refresh: 15
$ curl -i localhost:8000/preview-7f3a
HTTP/1.0 302 Found
Location: http://localhost:8000
Set-Cookie: laravel_maintenance=…; expires=…; Max-Age=43200; path=/; httponly; samesite=lax
$ php artisan up
 INFO Application is now live.

--with-secret generates the secret for you, --render="errors::503" pre-renders the page so it shows even while Composer dependencies are being updated, and --redirect=/ sends every request elsewhere. On several servers, set APP_MAINTENANCE_DRIVER=cache with a shared cache store so that one down affects all of them. Zero-downtime deployments (see Deployment) usually make maintenance mode unnecessary.

Applications created before Laravel 11

Applications created with Laravel 10 or older have a different structure, which upgrades do not change automatically:

Laravel 10 and older Laravel 11 and newer

app/Http/Kernel.php with $middleware, $middlewareGroups, $middlewareAliases

->withMiddleware() in bootstrap/app.php

app/Console/Kernel.php with schedule() and commands()

routes/console.php (schedule and closure commands) and ->withCommands()

app/Exceptions/Handler.php

->withExceptions()

app/Providers/RouteServiceProvider.php (HOME, rate limiters, route files)

->withRouting(); rate limiters in AppServiceProvider::boot()

Five providers (App, Auth, Broadcast, Event, Route) registered in config/app.php

One AppServiceProvider in bootstrap/providers.php

routes/api.php and routes/channels.php always present

Added by install:api and install:broadcasting

All configuration files published

Common ones published, the rest with config:publish

Upgraded applications keep working with the old structure, and the upgrade guides do not require moving to the new one. Migrating is optional and can be done piece by piece; tools such as Laravel Shift automate it. See What’s Changed and Upgrading.

Where older books and tutorials are outdated

  • app/User.php (Laravel 5.x and 7, as in Stauffer) moved to app/Models/User.php in Laravel 8.

  • Registering middleware in app/Http/Kernel.php, the schedule in app/Console/Kernel.php and route files in RouteServiceProvider (Stauffer ch. 10 and 16) is replaced by bootstrap/app.php and routes/console.php (Laravel 11).

  • env() calls in controllers or views break as soon as configuration is cached; read config() instead.

  • Committing .env "because it is private" is a credential leak; commit .env.example, or an encrypted .env.encrypted with the key stored elsewhere.