Laravel Folder Structure: How to Organize the app/ Directory (2026 Guide)
|

Laravel Folder Structure: How to Organize the app/ Directory (2026 Guide)

Reading Time: 6 minutes

Series: Every Laravel Project Should Have These Building Blocks 
Part: 1 of 35 | Level: Beginner | Prerequisites: Introduction


What You’ll Learn

  • Why the default Laravel structure breaks down after ~20 routes
  • The folder layout used in production apps
  • What belongs in each folder and why
  • The special role of app/Support/ as your app’s internal library
  • Common mistakes developers make with folder structure

The Problem with the Default Structure

When you run php artisan make:controller ProductController, Laravel creates a single app/Http/Controllers/ folder. That’s fine for a todo app.

In production, a real application might have 50+ controllers, 30+ services, 20+ jobs, and 15+ model traits. If everything lives in one flat folder, you spend more time searching for files than writing code.

Worse: when everything is in the same place, it’s tempting to put everything in the same class. Controllers grow fat. Models grow logic. Tests become impossible.

The structure in this article forces good habits by making the right place obvious.


The Full Folder Layout

Here is the complete app/ structure used across the projects in this series. We’ll explain each folder in detail below.

app/
├── Actions/
│   └── Order/
│       ├── CreateOrderAction.php
│       └── CancelOrderAction.php
│
├── Console/
│   └── Commands/
│       ├── HeartbeatCommand.php
│       └── PruneOldLogsCommand.php
│
├── DTOs/
│   ├── OrderCustomerData.php
│   └── ServiceCheckResult.php
│
├── Enums/
│   ├── CacheKeys.php
│   ├── PaymentStatus.php
│   └── ServiceStatus.php
│
├── Events/
│   └── OrderPlaced.php
│
├── Exceptions/
│   └── InsufficientStockException.php
│
├── Exports/
│   ├── BaseExport.php
│   └── OrdersExport.php
│
├── Http/
│   ├── Controllers/
│   │   ├── Admin/
│   │   │   └── DashboardController.php
│   │   └── Api/
│   │       └── V1/
│   │           └── OrderController.php
│   ├── Middleware/
│   │   ├── RequestLogger.php
│   │   └── SecureHeadersMiddleware.php
│   └── Requests/
│       ├── StoreOrderRequest.php
│       └── UpdateOrderRequest.php
│
├── Jobs/
│   ├── HeartBeat.php
│   └── ProcessPaymentJob.php
│
├── Listeners/
│   └── SendOrderConfirmation.php
│
├── Mail/
│   ├── ExceptionOccurred.php
│   └── OrderConfirmationMail.php
│
├── Models/
│   ├── Order.php
│   └── User.php
│
├── Notifications/
│   └── OrderShippedNotification.php
│
├── Observers/
│   └── OrderObserver.php
│
├── Policies/
│   └── OrderPolicy.php
│
├── Providers/
│   └── AppServiceProvider.php
│
├── Rules/
│   └── ValidCouponCode.php
│
├── Services/
│   ├── Core/
│   │   └── ErrorReporter.php
│   ├── Status/
│   │   ├── StatusService.php
│   │   ├── Contracts/
│   │   │   └── StatusServiceInterface.php
│   │   └── Checks/
│   │       ├── DatabaseCheck.php
│   │       └── CacheCheck.php
│   └── Order/
│       ├── OrderService.php
│       └── OrderPaymentService.php
│
└── Support/
    ├── Contracts/
    │   └── IsSluggable.php
    ├── Traits/
    │   ├── Console/
    │   │   └── LogsCommandMessages.php
    │   ├── ModelChangeLogger.php
    │   ├── Sluggable.php
    │   └── InteractsWithMedia.php
    └── File.php

Folder-by-Folder Breakdown

app/Actions/

What goes here: Single-purpose classes that perform one specific operation.

An Action is different from a Service. A Service is a collaborator you inject and call multiple times. An Action is executed once to complete one atomic operation.

// app/Actions/Order/CreateOrderAction.php
final readonly class CreateOrderAction
{
    public function __construct(
        private HostingPlanPricingService $pricingService
    ) {}

    public function execute(User $user, OrderCustomerData $data, Collection $cartItems): Order
    {
        return DB::transaction(function () use ($user, $data, $cartItems): Order {
            $order = Order::create([...]);
            $this->attachOrderItems($order, $cartItems);
            return $order;
        });
    }
}

Group actions in subdirectories when you have more than 3 related to the same domain: Actions/Order/Actions/ServiceRequest/Actions/Certificate/.


app/DTOs/

What goes here: Immutable data transfer objects – typed containers for passing data between layers.

DTOs replace raw arrays. Instead of passing $data['first_name'] and hoping the key exists, you pass a OrderCustomerData object with typed properties.

// app/DTOs/OrderCustomerData.php
final readonly class OrderCustomerData
{
    public function __construct(
        public string $firstName,
        public string $lastName,
        public string $email,
        public PaymentGateway $paymentGateway,
        public ?string $address = null,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            firstName: (string) ($data['first_name'] ?? ''),
            lastName: (string) ($data['last_name'] ?? ''),
            email: (string) ($data['email'] ?? ''),
            paymentGateway: PaymentGateway::tryFrom($data['payment_gateway'] ?? '') ?? PaymentGateway::Unknown,
        );
    }
}

The readonly modifier (PHP 8.1+) makes properties unmodifiable after construction – perfect for data you want to pass around without mutation.


app/Enums/

What goes here: Typed constants that represent a fixed set of values.

Enums replace magic strings like 'status' => 'pending'. They give you IDE autocompletion, static analysis type checking, and the ability to attach behavior to values.

// app/Enums/CacheKeys.php
enum CacheKeys: string
{
    case DashboardOrders = 'admin.dashboard.orders';
    case DashboardClaims = 'admin.dashboard.claims';

    public function forYear(int $year): string
    {
        return sprintf('%s.%d', $this->value, $year);
    }
}

// Usage
Cache::flexible(CacheKeys::DashboardOrders->value, [1800, 3600], fn () => ...);
Cache::flexible(CacheKeys::DashboardOrders->forYear(2024), [1800, 3600], fn () => ...);

app/Services/

What goes here: Stateless domain logic that doesn’t fit in a single Action – things that query, compute, coordinate, or integrate with external APIs.

Organize services in subdirectories by domain: Services/Order/Services/Status/Services/Core/. The Services/Core/ directory is special – it holds infrastructure services like ErrorReporter that cut across the entire application.

Services can have contracts (interfaces). When they do, bind the interface to the implementation in AppServiceProvider.

// app/Services/Order/OrderService.php
final class OrderService
{
    public function __construct(
        private readonly StripePaymentServiceInterface $stripe,
    ) {}

    public function processPayment(Order $order, string $paymentIntentId): void
    {
        // coordinate between Stripe and the order
    }
}

app/Support/

What goes here: Shared utilities, reusable traits, and contracts that are used across multiple parts of the application.

This is the most important folder most developers skip. Think of app/Support/ as a private package that lives inside your app. It’s where you put things that don’t belong to any one domain but are used by many.

app/Support/
├── Contracts/            # Interfaces that define behaviour
│   └── IsSluggable.php
├── Traits/
│   ├── Console/
│   │   └── LogsCommandMessages.php  # Dual console+log output
│   ├── ModelChangeLogger.php         # Audit trail for any model
│   ├── Sluggable.php                 # Auto-generate URL slugs
│   ├── HasAddress.php                # Address fields mixin
│   └── InteractsWithMedia.php        # Media upload/retrieval
└── File.php                          # File utility: hash, MIME, safe names

The key distinction: Support/ is for infrastructure and cross-cutting concerns. If you find yourself writing the same trait for a second model, move it to Support/Traits/.


app/Http/Controllers/

What goes here: HTTP handlers – nothing more.

A controller’s job is: receive a request → validate → authorize → delegate to a service or action → return a response. That’s it. No business logic, no database queries, no loops.

Group controllers in subdirectories by role or domain: Controllers/Admin/Controllers/Api/V1/Controllers/Domain/.


app/Http/Requests/

What goes here: Form Requests that handle both validation (rules()) and authorization (authorize()).

Every form submission or API write operation should have its own Form Request. Group them to match their controllers.


app/Observers/

What goes here: Eloquent event hooks for model lifecycle events (created, updated, deleted, etc.) that need to trigger side effects.

Observers are better than putting logic in model boot methods because they’re in a separate class, they’re testable, and they don’t bloat your model file.


app/Policies/

What goes here: Authorization logic – who is allowed to do what to which resource.

Every Model that a user can act on should have a Policy. Register policies in AppServiceProvider or let Laravel auto-discover them.


Common Mistakes to Avoid

Mistake 1: Putting everything in one flat folder. Controllers/ProductController.phpControllers/UserController.phpControllers/AdminDashboardController.php – all in one folder with 40 files. Use subdirectories.

Mistake 2: Skipping app/Support/. When you need the same trait in three models, you copy-paste it. Then you fix a bug in one copy and forget the other two. That’s what Support/Traits/ is for.

Mistake 3: Using app/Helpers/ as a dumping ground. A helpers.php file with 50 functions is a god-file. Every function in it should either be a method on the appropriate class, or a static method on an appropriate support class.

Mistake 4: Not grouping services by domain. Services/CreateUserService.phpServices/UpdateUserService.phpServices/DeleteUserService.php – three files for one domain. Use Services/User/ as a subdirectory.


Frequently Asked Questions

Where should I put business logic in Laravel?
Business logic does not belong in controllers or models. Put single, atomic operations (like “create an order”) in an Action class under app/Actions/, and put logic that queries, coordinates, or integrates with external services in a Service class under app/Services/, grouped by domain.
Controllers should only receive the request, delegate to the Action or Service, and return a response. Models hold relationships, casts, and scopes — not workflow logic. The rule of thumb: if it is one task, it is an Action; if it spans multiple steps or models, it is a Service.

Is it OK to create custom folders in Laravel’s app directory?
Yes. Laravel does not restrict you to the default folders. Because everything under app/ is PSR-4 autoloaded, any folder you create (such as app/Actions/, app/DTOs/, app/Enums/, or app/Support/) works automatically as long as the namespace matches the path.

Custom folders are encouraged once your app grows past a handful of routes – they keep related code together and make the right place to put a class obvious. Just keep namespaces consistent with the directory structure.

What is the app/Support folder in Laravel?
app/Support/ is your application’s internal library – the place for shared utilities, reusable traits, and contracts that are used across multiple domains but do not belong to any single one.

Think of it as a private package living inside your app: trait mixins like Sluggable or ModelChangeLogger, interfaces under Support/Contracts/, and helper classes like a File utility. It is the folder most developers skip, and the one that prevents copy-pasting the same trait into three different models.

How should I organize controllers in a large Laravel app?
Group controllers into subdirectories by role or domain instead of one flat folder. Common patterns are Http/Controllers/Admin/, Http/Controllers/Api/V1/, and domain folders like Http/Controllers/Order/.

Each subdirectory gets its own namespace. This keeps a 40-controller app navigable and makes route definitions read clearly. Keep every controller thin – it should validate, authorize, delegate, and respond, nothing more.

Key Takeaways

  • The folder structure forces good habits. When there’s an obvious right place for code, you’re less likely to put it in the wrong place.
  • app/Support/ is your internal library. Use it to share code across domains without coupling domains to each other.
  • Group by domain within each folder when you have more than 3–4 related files.
  • Controllers belong in Http/Controllers/ only if they handle HTTP. Don’t let them grow past their single responsibility.

Tips and Gotchas

⚠️ Warning: Don’t over-engineer structure before your first feature. Start with the layout above, move things as they grow. Premature domain splitting leads to empty folders and awkward imports.

💡 Tip: If you’re unsure where a class belongs – “Is this HTTP-specific?” → Http/. “One task?” → Actions/. “Spans multiple models?” → Services/. “Shared utility?” → Support/.

🔥 Expert Note: app/Support/ is your internal lib/ folder. It’s code everything depends on but that doesn’t own any domain. Keep it separate to prevent circular dependencies.

Further Reading


← Introduction | Next: Configuration Strategy →

Laravel Folder Structure: How to Organize the app/ Directory (2026 Guide)

Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox.

We don’t spam! Read our privacy policy for more info.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

6 Comments