Skip to content
beginner Phase 13 · Laravel Framework Fundamentals

Creating a Laravel Project

Scaffold an application and learn the directory layout.

35m
0 problems
Topic Progress 0%

What composer create-project Actually Downloads and Generates

You start a Laravel app the same way for a side project or a bank's internal tool: one Composer command that pulls the framework plus a working skeleton into a new directory. The command is short; understanding what it does is the difference between debugging an install and re-running it and hoping. You get here straight from Coverage Reports and Thresholds: a green suite against fixtures, and now a real application for it to exercise.

composer create-project laravel/laravel shop
# pin a version line:
composer create-project laravel/laravel shop "^12.0"
# pre-release work:
composer create-project laravel/laravel shop "dev-master"

Laravel 11 and 12 both require PHP 8.2 or newer; the command fails loudly if your PHP is older, with Composer reporting the platform requirement php >=8.2.0 as unsatisfiable. create-project does more than copy files. It resolves laravel/laravel (the skeleton package, not the framework itself) from Packagist, then installs laravel/framework and its whole dependency tree — Carbon, Symfony components, Monolog, the Blade compiler, Eloquent — into vendor/, and finally runs the skeleton's post-create scripts.

The skeleton and the framework are deliberately separate packages. The skeleton holds the directory layout, .env.example, artisan, and default configuration. The framework holds the actual Illuminate\* classes. That split is why upgrading Laravel is usually a Composer update of laravel/framework, not a re-download of your project shape.

Those post-create scripts matter more than people expect. The skeleton's composer.json declares:

"scripts": {
    "post-create-project-cmd": [
        "@php artisan key:generate --ansi",
        "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
    ]
}

So the moment installation finishes, Laravel copies .env.example to .env and generates a random APP_KEY. If you create a project with --no-scripts, you must run php artisan key:generate yourself — an empty APP_KEY is the single most common cause of MissingAppKeyException on a fresh install. Sessions and encrypted cookies are encrypted with that key; without one the framework refuses to boot rather than silently use a weak value.

You can watch the install in stages when you want to understand it. composer create-project laravel/laravel shop --no-install only downloads the skeleton. Then inside shop/ you run composer install yourself, copy .env.example to .env, and run php artisan key:generate. Same result, every step visible — which is exactly what you want when Composer behaves differently on a CI runner than on your laptop.

Two flags come up constantly in team settings:

Flag Effect When it matters
--prefer-dist Install zipped archives instead of git clones Faster, no git needed on CI
--no-interaction Never prompt Non-interactive build agents
--no-scripts Skip post-create hooks Custom provisioning pipelines

Composer also writes composer.lock, and the file is not optional: it pins the exact versions of every dependency that resolved today. Commit it. Without the lock file, composer install on another machine resolves fresh versions and your team's "same" application runs different framework code. A useful verification once the command finishes is composer validate plus a quick php artisan --version inside the new directory — you should see the framework version printed, which proves both the install and the autoloader work.

After the command completes you have an application that runs — no manual wiring, no front controller to write. The next question is what all those directories mean, and which of them you actually touch daily.

Reading the Laravel 11/12 Layout: Which Directories the Framework Expects

Laravel 11 cut the skeleton down hard: a fresh install ships with a slimmed tree, and several files that used to exist (like app/Http/Kernel.php) were removed in favor of configuration inside bootstrap/app.php. Knowing the modern layout stops you from hunting for files that no longer exist.

Directory What lives there You touch it when
app/Models Eloquent model classes Almost every feature
app/Http/Controllers Controller classes Almost every feature
app/Http/Middleware Request middleware Auth, rate limiting, headers
routes/ web.php, api.php, console.php, channels.php Every feature: URLs are declared here
resources/views Blade templates Rendering HTML
database/migrations Schema definitions as PHP classes Changing tables
database/factories, database/seeders Test data generators Seeding and testing
storage/ Logs, cache, compiled views, uploads Runtime writes (never in git)
bootstrap/cache Package discovery and config caches Runtime writes (never in git)
public/ index.php, built assets, favicon.ico Deploy: this is the only directory the web server exposes
config/ Configuration arrays Only when overriding a default
tests/ PHPUnit feature and unit tests Every feature
vendor/ Installed packages Never edit by hand

bootstrap/app.php is the new nerve center. In Laravel 11+ it looks like this:

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

There is no Kernel.php to open — middleware groups, exception rendering, and route registration all live here as fluent method calls. When someone asks you to "register a middleware globally" in Laravel 11+, this file is the answer. The health: '/up' route is new too: a tiny endpoint that returns 200 with no dependencies, which load balancers and uptime monitors can hit without touching your database.

config/ is thinner than older tutorials suggest. Framework defaults now ship inside the framework package itself, and you pull a config file into your project only when you need to change it:

php artisan config:publish
php artisan config:publish cache

The storage/ tree deserves one more warning because it is where "it works locally" bugs hide. Inside it, storage/framework/views holds compiled Blade templates, storage/framework/cache holds cache data, and storage/logs holds laravel.log. If compiled views go stale after you edit a template in an environment that caches aggressively, php artisan view:clear drops them and the next request recompiles; if permissions on storage/logs are wrong, the framework throws a Log directory not found exception before any of your code runs. On Linux deploys that means chowning the tree to the PHP-FPM user — a step Windows and macOS development never forces you to learn.

Two hard rules about directories survive every version bump. Point your web server's document root at public/, never the project root — that keeps .env, vendor/, and app/ unreachable over HTTP. And treat storage/ and bootstrap/cache/ as runtime state: they must be writable by the PHP user and excluded from version control, because every deploy overwrites code but must preserve these. A deploy that rm -rfs the whole directory before extraction wipes compiled views and cached config, producing a brief 500 until artisan rebuilds them.

Environment Configuration: .env, APP_KEY, and the First Artisan Commands

The .env file is how one codebase runs on three machines with three databases. Laravel loads it through vlucas/phpdotenv into $_ENV, and your config/*.php files read it with the env() helper:

APP_NAME=Shop
APP_ENV=local
APP_KEY=base64:2mJ...32bytes...
APP_DEBUG=true
APP_URL=http://localhost

DB_CONNECTION=sqlite
DB_DATABASE=database/database.sqlite

SESSION_DRIVER=database
QUEUE_CONNECTION=sync

Notice DB_CONNECTION=sqlite: Laravel 11+ defaults to SQLite for local development, so a fresh install needs no database server at all — just touch database/database.sqlite && php artisan migrate to create the schema. Switching to MySQL or Postgres is four lines in .env (DB_CONNECTION, DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD) and no code change, because the connection is read from config, not hardcoded.

The separation rule that confuses newcomers: env() belongs in config/ files only, never in application code. Once you run php artisan config:cache, the values are baked into bootstrap/cache/config.php and env() returns null everywhere else. Code that calls env('APP_NAME') directly works in development and silently breaks in production — read config('app.name') instead. This is one of the most common Laravel interview tripping points, and the fix is a one-line habit: configuration is read from config/, not from .env.

You can see the wiring yourself by opening config/app.php after publishing it. The name key returns env('APP_NAME', 'Laravel') — the env() call happens there, once, at config load time, and the rest of the application only ever asks the container for config('app.name'). The same pattern holds for database.default, cache.default, and mail.default.

Environment-specific behavior goes through APP_ENV:

  • APP_ENV=local + APP_DEBUG=true: full stack traces in the browser.
  • APP_ENV=production: debug pages off, generic error page on.
  • @env('local') in Blade and if (app()->isProduction()) in PHP branch on it.

.env must never be committed — .env.example holds the shape of the configuration with placeholder values, while the real .env stays out of git. The default .gitignore already excludes it; teams get burned when someone force-adds it and pushes a database password to a public repo. Rotate any key that ever touches a public history.

Now verify the install with artisan, the CLI that ships with every Laravel project (php artisan with no arguments prints the full command list):

php artisan key:generate        # only if APP_KEY is empty
php artisan serve               # http://127.0.0.1:8000
php artisan serve --host=0.0.0.0 --port=8080   # VM or container work
php artisan about               # environment, PHP version, cache status
php artisan route:list          # every registered route, with its middleware
php artisan migrate             # apply migrations to the .env database

php artisan serve runs PHP's single-threaded dev server on port 8000 — fine for development, never for production (that job belongs to Nginx and PHP-FPM). If port 8000 is taken, Address already in use is the tell; pass --port=8001. And when .env changes after you enable caching, run php artisan config:clear before config:cache, or you will wonder why your edits do nothing.

With the skeleton installed, the environment configured, and artisan answering, you are ready to map URLs to code in Routing and Controllers.

One last habit worth building on day one: run php artisan about after every environment change. It prints the environment name, Laravel and PHP versions, and — the part that matters — whether the application, cache, and config caches are booted and filled. When a deploy misbehaves, that one screen tells you in seconds whether the server is on the right .env, the right PHP, and a warm config cache, before you touch a log file.

Quiz

1. A freshly created Laravel project throws MissingAppKeyException on the first request. The .env file exists. What is the cause and fix?

Question 1 options

2. In a Laravel 11/12 application, where does the code that registers web middleware groups and route loading live?

Question 2 options

3. A developer writes $name = env('APP_NAME'); inside a controller. It works locally but returns null in production after php artisan config:cache. Why?

Question 3 options

4. You run php artisan serve in a VM and cannot reach the app from your host browser at http://127.0.0.1:8000. Which change addresses it?

Question 4 options

Flashcards

Question

What does composer create-project laravel/laravel actually install?

Answer

The laravel/laravel skeleton package (directory layout, .env.example, artisan, composer scripts) plus laravel/framework and its full dependency tree into vendor/. The post-create scripts copy .env.example to .env and run artisan key:generate, so the app boots immediately.

Question

Why does Laravel refuse to boot with an empty APP_KEY, and how do you fix it?

Answer

APP_KEY encrypts sessions, cookies, and any encrypted data; without it the framework throws MissingAppKeyException rather than run unencrypted. Fix with php artisan key:generate, which writes a fresh base64 key into .env.

Question

Where should env() be called in a Laravel application?

Answer

Only inside config/*.php files. Application code must read config('app.key'). After php artisan config:cache, .env is not loaded, so env() outside config files returns null in production.

Question

What is the correct document root for a Laravel deployment, and why?

Answer

The public/ directory. It exposes only index.php and static assets, keeping .env, vendor/, app/, and database files unreachable over HTTP. In development, php artisan serve serves the same public/ directory on 127.0.0.1:8000.

Question

How do you change the development server's host and port, and what causes Address already in use?

Answer

php artisan serve --host=0.0.0.0 --port=8080. The defaults are 127.0.0.1:8000, so Address already in use means another process (often a forgotten dev server) holds port 8000; pick another port or stop the process. Use --host=0.0.0.0 when the app must be reachable from outside a VM or container.

Revision Notes

Key Takeaways

  • 1. create-project installs the laravel/laravel skeleton plus the framework, and its post-create scripts provision .env and APP_KEY so the app boots immediately
  • 2. An empty APP_KEY throws MissingAppKeyException — key:generate fixes it; sessions and cookies depend on that key
  • 3. Laravel 11+ removed app/Http/Kernel.php: routing, middleware, and exceptions are configured in bootstrap/app.php
  • 4. Call env() only inside config/ files; after config:cache it returns null elsewhere, so application code reads config('...')
  • 5. public/ is the document root and storage/ plus bootstrap/cache/ are writable runtime state excluded from version control

Interview Tips

  • • "Walk me through what create-project does" — name the skeleton package, the framework install into vendor/, and the post-create scripts; most candidates stop at 'it downloads Laravel'
  • • "Why is my env() call null in production?" — answer with the config cache: .env is not loaded once config is cached, so env() outside config files breaks; show you know the fix is config('key')
  • • "What's in bootstrap/app.php in Laravel 11?" — withRouting, withMiddleware, withExceptions; mention the Kernel.php removal and you immediately stand out
  • • "How do you secure a Laravel deployment?" — document root at public/, .env untracked, APP_DEBUG=false with APP_ENV=production, and config:cache plus route:cache on release

Cheat Sheet

Creating a Laravel Project — Cheat Sheet

Scaffold:

composer create-project laravel/laravel shop      # PHP 8.2+ required
cd shop
# post-create scripts: copy .env.example -> .env, artisan key:generate
php artisan serve                    # 127.0.0.1:8000
php artisan serve --host=0.0.0.0 --port=8080
php artisan about | route:list | migrate

Key directories:

Path Role
app/Models, app/Http/Controllers daily-edit code
routes/web.php, routes/api.php URL → handler declarations
resources/views Blade templates
database/migrations schema as PHP classes
storage/, bootstrap/cache/ runtime state — writable, not in git
public/ the only web-exposed directory
config/ override framework defaults via config:publish

Bootstrap (Laravel 11+): bootstrap/app.php — withRouting(), withMiddleware(), withExceptions() replace the old Kernel.php files.

Environment rules:

  • .env in git: never; commit .env.example as the template
  • env() in config files only; app code reads config(...)
  • config:cache bakes config to a file — config:clear before re-caching after .env edits
  • APP_ENV/APP_DEBUG control environment branches and debug pages
  • Default local DB is SQLite: touch database/database.sqlite && php artisan migrate

Continue Learning

Related Reading