Stu Mason
Stu Mason
Guide

Laravel Folder Structure in 2026: What Goes Where and Why

Stuart Mason5 min read

If you've been writing Laravel since the 5.x days like I have, opening a fresh Laravel 12 project can feel a bit like someone's burgled your house. Where's the Kernel? Where did my middleware director

Laravel Folder Structure in 2026: What Goes Where and Why

If you've been writing Laravel since the 5.x days like I have, opening a fresh Laravel 12 project can feel a bit like someone's burgled your house. Where's the Kernel? Where did my middleware directory go? Why is bootstrap/app.php suddenly doing everything?

Deep breath. It's actually better this way.

The Old Way Is Dead

Laravel 10 and earlier had a app/Http/Kernel.php that registered middleware stacks. It had a app/Console/Kernel.php for scheduling. It had a app/Providers directory with five or six service providers by default. It had a app/Http/Middleware directory with half a dozen middleware files you'd never touch.

Most of that is gone. Not deprecated — gone.

Where Things Live Now

Here's the actual directory listing from the Progress site (the one you're reading right now):

app/
├── Actions/           # Business logic
│   ├── Article/
│   ├── Dashboard/
│   ├── FeedItem/
│   ├── Fortify/
│   ├── GoodFit/
│   ├── Newsletter/
│   ├── Repository/
│   ├── Stats/
│   └── Tag/
├── Concerns/          # Traits
├── Console/
│   └── Commands/      # Artisan commands (auto-registered)
├── DataTransferObjects/
├── Enums/
├── Events/
├── Helpers/
├── Http/
│   ├── Controllers/
│   ├── Middleware/     # Only custom middleware
│   └── Requests/      # Form requests
├── Jobs/
├── Mail/
├── Mcp/               # MCP server tools
├── Models/
├── Observers/
├── Policies/
├── Providers/
├── Rules/
├── Services/
└── Support/
bootstrap/
├── app.php            # THE central config file
├── cache/
├── providers.php      # Service providers list
└── ssr/

config/                # Standard config files
routes/
├── ai.php             # MCP routes
├── api.php
├── channels.php
├── console.php        # Scheduled tasks
├── settings.php
└── web.php

bootstrap/app.php Is the Boss Now

In Laravel 12, bootstrap/app.php is where you configure middleware, exception handling, and routing. It uses a fluent API:

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        channels: __DIR__.'/../routes/channels.php',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->web(append: [
            HandleInertiaRequests::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // Exception handling config
    })
    ->create();

No more Kernel.php. No more guessing which middleware stack something belongs to. It's all right here in one file.

Console Commands Auto-Register

This one catches people out. In Laravel 12, any command in app/Console/Commands/ is automatically discovered and registered. You don't need to list them anywhere. Just create the class, and it works.

The routes/console.php file is still there, but it's for closure-based commands and scheduled tasks, not for registering command classes.

Service Providers Are Minimal

Check bootstrap/providers.php:

return [
    App\Providers\AppServiceProvider::class,
    // Any other providers you actually need
];

That's it. You don't get five providers by default any more. You get one, and you add more only if you need them. Most apps only need AppServiceProvider and maybe one or two domain-specific ones.

The Actions Pattern

This isn't a Laravel convention per se — it's a pattern I use on every project. Business logic goes into single-responsibility Action classes, not controllers or models.

// app/Actions/Article/PublishArticle.php
class PublishArticle
{
    public function execute(Article $article): void
    {
        $article->update([
            'published_at' => now(),
            'status' => ArticleStatus::Published,
        ]);

        // Fire events, notify subscribers, etc.
    }
}

Controllers stay thin — they validate, call an Action, and return a response. Models stay focused on relationships and scopes. The Action is where the actual work happens.

On the Progress site, I've got Actions organised by domain: Article/, Dashboard/, FeedItem/, Repository/, and so on. Each directory maps to a feature area of the app.

DTOs for Data Transfer

When you're passing data from the backend to Inertia pages, Data Transfer Objects keep things clean:

// app/DataTransferObjects/ArticleData.php
readonly class ArticleData
{
    public function __construct(
        public int $id,
        public string $title,
        public string $slug,
        public ?string $excerpt,
        public Carbon $publishedAt,
    ) {}

    public static function fromModel(Article $article): self
    {
        return new self(
            id: $article->id,
            title: $article->title,
            slug: $article->slug,
            excerpt: $article->excerpt,
            publishedAt: $article->published_at,
        );
    }
}

Then in your controller: Inertia::render('Articles/Show', ['article' => ArticleData::fromModel($article)]). The frontend gets exactly the data it needs, with TypeScript types generated automatically.

Services vs Actions

I get asked this a lot. Here's my rule of thumb:

  • Actions: Single operations. PublishArticle, CreateUser, SyncRepository. One public method, one job.
  • Services: Wrappers around external APIs or complex subsystems. GitHubService, StripeService. Multiple methods, stateful configuration.

If it talks to a third-party API, it's probably a Service. If it orchestrates your own business logic, it's an Action.

Where Tests Go

tests/
├── Browser/    # Pest 4 browser tests
├── Feature/    # Feature/integration tests
└── Unit/       # Unit tests

Feature tests are the workhorse. Unit tests for isolated logic. Browser tests for critical user flows. On the Progress site, most tests are feature tests — they hit real routes with real data and assert real responses.

What About Middleware?

Custom middleware still lives in app/Http/Middleware/. The difference is you only create files for middleware you've actually written. The default middleware (auth, CSRF, etc.) is configured in bootstrap/app.php without needing dedicated files.

The Config Directory

Still the same as it always was: one file per concern. config/app.php, config/database.php, config/queue.php, etc. The only change is that some packages that used to publish config files now have sensible defaults that don't need publishing.

On this project, you'll notice config/coolify.php and config/sqids.php — package configs that needed customisation. Everything else is standard Laravel.

My Advice

If you're starting a new project in 2026, embrace the slim structure. Don't recreate the old directories out of habit. The app/Http/Kernel.php file isn't coming back, and that's a good thing.

Organise your app directory by what makes sense for your domain. Actions, DTOs, Enums, Services — whatever patterns serve your codebase. Laravel gives you the foundation; the rest is up to you.

And for the love of God, don't put business logic in your controllers.


I write about Laravel, AI tooling, and the realities of building software products. If you found this useful, there's more on stuartmason.co.uk.

Get the Friday email

What I shipped this week, what I learned, one useful thing.

No spam. Unsubscribe anytime. Privacy policy.