# Why I Built My Own Laravel Modules Package

**Author:** Mozex | **Published:** 2026-03-27 | **Tags:** Laravel, PHP, Open Source, Architecture | **Package:** mozex/laravel-modules | **URL:** https://mozex.dev/blog/4-why-i-built-my-own-laravel-modules-package

---


In late 2023, I was working on a large Laravel project and hit a point where the `app/` directory had become a mess. Models, controllers, services, policies, listeners, jobs, notifications, all thrown into flat directories with no logical grouping. Everything related to billing lived next to everything related to user management. Finding anything required memorizing file names instead of following a structure.

I knew the answer was modular architecture. Break the application into self-contained modules where each module owns its models, controllers, migrations, routes, views, and everything else related to that feature. I'd seen this pattern work in other frameworks and it made perfect sense for Laravel.

So I looked at what was available. I tried several packages. None of them clicked.

I started building my own solution inside that project. As it grew and I wanted the same structure in my other projects, I extracted it into an open-source package: [mozex/laravel-modules](https://github.com/mozex/laravel-modules).

<!--more-->

## What Is Modular Architecture?

If you haven't worked with modules before, the concept is straightforward. Instead of organizing code by type (all models in one folder, all controllers in another), you organize by feature or domain.

A standard Laravel project looks like this:

```
app/
├── Models/
│   ├── User.php
│   ├── Post.php
│   ├── Comment.php
│   ├── Order.php
│   ├── Product.php
│   └── Invoice.php
├── Http/Controllers/
│   ├── UserController.php
│   ├── PostController.php
│   ├── OrderController.php
│   └── ...
├── Policies/
│   ├── UserPolicy.php
│   ├── PostPolicy.php
│   └── ...
└── ...
```

A modular project looks like this:

```
Modules/
├── Blog/
│   ├── Models/
│   │   ├── Post.php
│   │   └── Comment.php
│   ├── Http/Controllers/
│   │   └── PostController.php
│   ├── Policies/
│   │   └── PostPolicy.php
│   └── ...
├── Billing/
│   ├── Models/
│   │   ├── Order.php
│   │   ├── Product.php
│   │   └── Invoice.php
│   └── ...
└── ...
```

Everything about billing lives together. Everything about the blog lives together. When you need to work on billing, you go to `Modules/Billing` and everything is right there. When you want to remove a feature entirely, you delete one directory instead of hunting through twenty folders.

No fancy design patterns, no DDD buzzwords. Just grouping related code together.

## Why Not the Existing Packages?

The most popular Laravel modules package is [nWidart/laravel-modules](https://github.com/nWidart/laravel-modules), with over 14 million downloads. It deserves that popularity. The nWidart team has maintained this package for years, built a real community around it, and introduced a lot of developers to modular architecture in Laravel. That's genuinely impressive work, and I have respect for what they've built.

But when I tried it for my own projects, the approach didn't match how I wanted to work.

nWidart's package follows an explicit registration model. Each module needs a third-party Composer plugin (`wikimedia/composer-merge-plugin`) for autoloading, its own `module.json` manifest, its own `composer.json`, and three service providers: a main service provider, a route service provider, and an event service provider. The package also ships with 67 Artisan commands for scaffolding every conceivable file type.

That's a thorough, well-structured approach. For teams that want full control over every registration step, it makes sense.

For me, it was more ceremony than I wanted. Here's what a freshly generated nWidart module gives you:

```
Modules/Blog/
├── app/
│   ├── Http/Controllers/
│   └── Providers/
│       ├── BlogServiceProvider.php
│       ├── EventServiceProvider.php
│       └── RouteServiceProvider.php
├── config/config.php
├── database/
├── resources/
├── routes/
├── tests/
├── composer.json       # Per-module autoloading
├── module.json         # Module manifest
├── package.json
└── vite.config.js
```

Every new module needs those service providers. Every route, config, view namespace, Blade component, Artisan command, and event listener has to be wired in the module's service provider by hand. The package auto-discovers migrations and translations, but everything else requires manual registration.

I also found that the 67 Artisan commands for file generation weren't something I needed. PhpStorm with the [Laravel Idea](https://laravel-idea.com/) plugin already creates files in the correct module directory without any package support. VS Code has similar extensions. Your IDE already understands your project structure.

The issue wasn't quality. nWidart is a solid, well-tested package. The issue was philosophy. I wanted something that followed Laravel's own direction: convention over configuration, auto-discovery over manual registration. So I built my own.

## Convention Over Configuration

I created [mozex/laravel-modules](https://github.com/mozex/laravel-modules) with one guiding principle: if you follow Laravel's conventions, the package should figure out the rest.

No manifest files. No per-module service providers. No Composer plugins. No artisan commands to scaffold modules.

You install the package, add one PSR-4 autoload entry to your `composer.json`, and start creating modules. The package discovers everything based on your directory structure.

```bash
composer require mozex/laravel-modules
```

Add the autoload mapping:

```json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Modules\\": "Modules/",
            "Database\\Seeders\\": "database/seeders/"
        }
    }
}
```

Run `composer dump-autoload`. Done.

Create a `Modules/Blog` directory, put a model in `Modules/Blog/Models/Post.php`, and it works. Add a migration in `Modules/Blog/Database/Migrations/`, and `php artisan migrate` picks it up. Create a route file at `Modules/Blog/Routes/web.php`, and your routes register with the `web` middleware automatically. No service provider needed. No registration code. Nothing.

## What Gets Auto-Discovered

This is where the package earns its keep. It auto-discovers 17 different types of module assets.

The package doesn't force you into any particular frontend stack. Livewire, Inertia, Vue, React, plain Blade templates, it all works. The modular structure organizes your backend code. If you happen to use Livewire or Filament, the package discovers those components too. If you don't, it skips them silently and you'll never know the feature exists.

### Routes and Configuration

**Routes** get auto-loaded and grouped by filename. `web.php` gets the `web` middleware, `api.php` gets the `api` prefix and middleware. Broadcasting channels go in `channels.php`, console commands in `console.php`. You can define custom route groups too, for things like admin panels with their own prefix and middleware stack.

**Configs** are merged into Laravel's configuration. A file at `Modules/Blog/Config/blog.php` becomes accessible via `config('blog.setting')`. You control whether module values take priority over app values or serve as defaults.

### Database and Models

**Migrations** register with Laravel's migrator. Standard `php artisan migrate` picks them up with no configuration.

**Models and Factories** are wired through namespace-based guessing. `Modules\Blog\Models\Post` maps to `Modules\Blog\Database\Factories\PostFactory` automatically. Nested namespaces work too. No manual `newFactory()` overrides needed.

**Seeders** are discoverable through a `Modules` facade. In your main `DatabaseSeeder`, call `$this->call(Modules::seeders())` and all module seeders run.

**Policies** follow the same namespace convention. `Modules\Blog\Models\Post` maps to `Modules\Blog\Policies\PostPolicy` through Laravel's policy auto-discovery. Zero configuration.

### Views and Components

**Views** register under a namespace matching the module name. `view('blog::posts.show')` maps to `Modules/Blog/Resources/views/posts/show.blade.php`.

**Anonymous Blade components** work too: `<x-blog::form.input />` maps to `Modules/Blog/Resources/views/components/form/input.blade.php`.

**Class-based Blade components** get registered with a module prefix. `Modules/Blog/View/Components/Post/Card.php` becomes `<x-blog::post.card />`.

### Everything Else

**Service Providers** in any module's `Providers/` directory register during the application boot cycle. No manual additions to `bootstrap/providers.php`.

**Commands** extending `Illuminate\Console\Command` register with Artisan automatically.

**Event Listeners** get discovered and attached to their events. Listeners in one module can handle events from any other module or the core app.

**Translations** support both PHP arrays and JSON files, registered under the module's namespace: `__('blog::messages.welcome')`.

**Helpers** (PHP files in `Helpers/`) get auto-loaded during boot, making functions globally available.

### Livewire Integration

If you use Livewire, the package supports all three component types in Livewire v4:

**Class-based components** in `Modules/Blog/Livewire/` register automatically. Use them with `<livewire:blog::posts />`.

**Single-file components** (PHP and Blade combined in one `.blade.php` file) go in `Resources/views/livewire/`. Same naming convention, same auto-discovery.

**Multi-file components** (a directory with `name.php` and `name.blade.php`) work the same way.

No `Livewire::component()` calls. No manual registration. Drop your component files in the right directory and use them.

### Filament Integration

If you build admin panels with Filament, this is where things get really good. The package auto-discovers:

- **Resources** (your CRUD panels)
- **Pages** (custom admin pages)
- **Widgets** (dashboard widgets)
- **Clusters** (grouped navigation items)

Each maps to a panel through the directory path. `Modules/Blog/Filament/Admin/Resources/PostResource.php` registers with the `admin` panel. `Modules/Blog/Filament/Dashboard/Widgets/StatsWidget.php` registers with the `dashboard` panel.

You can toggle each Filament feature independently. And if Filament isn't installed in your project, the package skips all Filament discovery silently. Same behavior for Livewire and Nova. None of these are required dependencies. The package detects what you have installed and adapts.

### Nova Integration

If you use Laravel Nova, the package auto-discovers Nova resources from your modules. Classes extending `Laravel\Nova\Resource` in any module are registered automatically during Nova's serving event. Like Livewire and Filament, Nova is completely optional. If it's not installed, the feature is skipped silently.

## The Full Module Structure

Here's what a complete module looks like in practice:

```
Modules/
└── Blog/
    ├── Config/
    │   └── blog.php
    ├── Console/Commands/
    │   └── PublishScheduledPosts.php
    ├── Database/
    │   ├── Factories/
    │   │   └── PostFactory.php
    │   ├── Migrations/
    │   │   └── 2024_01_15_create_posts_table.php
    │   └── Seeders/
    │       └── BlogDatabaseSeeder.php
    ├── Events/
    │   └── PostPublished.php
    ├── Filament/
    │   └── Admin/
    │       ├── Resources/
    │       │   └── PostResource.php
    │       └── Widgets/
    │           └── PostStatsWidget.php
    ├── Helpers/
    │   └── blog_helpers.php
    ├── Lang/
    │   ├── en/
    │   │   └── messages.php
    │   └── en.json
    ├── Listeners/
    │   └── SendPostNotification.php
    ├── Livewire/
    │   └── PostEditor.php
    ├── Models/
    │   └── Post.php
    ├── Policies/
    │   └── PostPolicy.php
    ├── Providers/
    │   └── BlogServiceProvider.php
    ├── Resources/views/
    │   ├── components/
    │   │   └── post-card.blade.php
    │   ├── livewire/
    │   │   └── comment-section.blade.php
    │   └── posts/
    │       ├── index.blade.php
    │       └── show.blade.php
    ├── Routes/
    │   ├── web.php
    │   └── api.php
    └── View/Components/
        └── FeaturedPost.php
```

You don't need all of these. A module can be as small as a single model and a migration. Or as full-featured as the example above. The package discovers whatever you put in there.

## Portable Modules: The Biggest Win

Here's what convinced me to install this package on every project, even the small ones.

When your modules are self-contained, they become portable. I have a billing module that handles subscriptions, invoices, and payment processing. I built it for one project, refined it over time, and now I use it across several others. Copying it is as simple as dropping the `Billing/` directory into the new project's `Modules/` folder.

Same with my support module (a ticketing system). The backend logic, models, migrations, routes, policies, events, and Filament resources all live inside one directory. When another project needs a support system, I copy the module over, maybe adjust some frontend views, and everything else works immediately. The hard part, the backend logic and database structure, travels with the module.

This changes how you think about building features. When I'm working on a module and catch myself thinking "am I over-engineering this?", I remember that this module will probably end up in two or three other projects. The extra time to make it clean and self-contained pays for itself quickly.

Without modular architecture, reusing features means extracting code from scattered directories, updating namespaces, tracking down every migration and config file and route definition. With modules, it's a directory copy.

## Module Activation and Ordering

Sometimes you need to disable a module or control load order. The config file handles this cleanly:

```php
'modules' => [
    'Shared' => ['active' => true, 'order' => 1],
    'Blog'   => ['active' => true, 'order' => 2],
    'Legacy' => ['active' => false],
],
```

Disabled modules are invisible to the application. No routes, no migrations, no commands. And since this lives in the config file (not a separate JSON file at the project root), it follows the same caching and environment patterns as everything else in Laravel.

## Performance

Discovery has a cost. Scanning directories and resolving classes on every request would be wasteful in production. The package includes caching:

```bash
php artisan modules:cache    # Build the discovery cache
php artisan modules:clear    # Clear it when you make changes
```

In production, cache the discovery results alongside your config and route caches. In development, the package scans fresh on every request so changes are picked up immediately.

These are the only two Artisan commands the package ships.

## AI-Ready Development

If you use AI coding assistants (and at this point, who doesn't?), the package ships with built-in [Laravel Boost](https://laravel.com/ai/boost) guidelines. When you install the package, your AI agent automatically gets a full reference for modular development: where to put files, where to find them, how auto-discovery works, the directory conventions, and the complete facade API.

This means tools like Claude Code, Cursor, or GitHub Copilot can add features and maintain your codebase with full knowledge of modular structure. They know that a new model goes in `Modules/Blog/Models/`, that its factory belongs in `Modules/Blog/Database/Factories/`, and that routes go in `Modules/Blog/Routes/web.php`. No manual context-setting required.

## Switching From nWidart

If you're using nWidart's package and want to try a different approach, the module directory structures are similar enough that migration is doable. The main work is removing boilerplate:

1. Delete the three service providers from each module
2. Remove `module.json` and per-module `composer.json` files
3. Remove the `wikimedia/composer-merge-plugin` dependency
4. Adjust directory paths if needed (nWidart uses `app/` subdirectory by default, this package uses direct directories)
5. Add the PSR-4 autoload entry

You'll end up with less code and the same (or better) functionality. Everything those service providers were doing manually, the package now does through auto-discovery.

## Getting Started

The current v3 release supports PHP 8.3+, Laravel 11/12/13, Livewire v4, and Filament v5. If you're on older versions (PHP 8.2, Laravel 10, Livewire v3, or Filament v3/v4), the [2.x branch](https://github.com/mozex/laravel-modules/tree/2.x) has you covered.

```bash
composer require mozex/laravel-modules
```

Add the namespace to your `composer.json`:

```json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Modules\\": "Modules/",
            "Database\\Seeders\\": "database/seeders/"
        }
    }
}
```

```bash
composer dump-autoload
```

Create your first module:

```bash
mkdir -p Modules/Blog/Models
```

Add a model, a migration, a route file. The package handles the rest. No configuration files to publish (unless you want to customize the defaults), no service providers to create, no manifest files to maintain.

Check the [documentation](https://github.com/mozex/laravel-modules/tree/main/docs) for the full feature list and configuration options.

## Why I Install It on Every Project

I've been asked why I bother with modular architecture on small projects. My honest answer: I've never started a project knowing it would stay small.

Features get added. Scope changes. A "simple" CRUD app turns into something with billing, notifications, reporting, and an admin panel. When that happens, having modular structure from day one means you're already organized. You don't need to stop and refactor everything. You just keep building.

And because the package is zero-config, there's no overhead to adding it early. Installing it adds nothing to your setup time. You don't pay a complexity tax until you actually create modules. It's there when you need it, invisible when you don't.

I built this package for myself. I've used it in big projects and small ones, and it's been battle-tested across all of them. The discovery process is fully cached, performance-optimized, and has handled every scenario I've thrown at it.

If you're interested, check out the [GitHub repository](https://github.com/mozex/laravel-modules) and the [full documentation](https://github.com/mozex/laravel-modules/tree/main/docs). And if you try it, I'd love to hear how it works for your projects.