# Module Best Practices
Estimated time: 8 minutes
Difficulty: Intermediate
Building good modules isn't just about making them work — it's about making them maintainable, testable, and enjoyable for other developers (including future you).
Do These
1. One Responsibility Per Module
Each module should handle one feature domain only.
Newsletter module → email subscriptions
Blog module → content management
❌ BlogNewsletter → mixing two concerns
2. Prefix ALL Database Tables
// Good — prefix with module name
Schema::create('blog_posts', function (Blueprint $table) { ... });
Schema::create('blog_categories', function (Blueprint $table) { ... });
// ❌ Bad — generic names cause conflicts
Schema::create('posts', ...);
Schema::create('categories', ...);
3. Always Use Module View Namespaces
{{-- Good — explicit namespace --}}
@extends('blog::layouts.base')
@include('blog::partials.card')
{{-- ❌ Bad — implicit, breaks when moved --}}
@extends('layouts.app')
4. Put Business Logic in Services
// Good
class SubscribeController extends Controller
{
public function __construct(private NewsletterService $newsletter) {}
public function store(Request $request): JsonResponse
{
$this->newsletter->subscribe($request->email);
return response()->json(['message' => 'Subscribed!']);
}
}
// ❌ Bad — business logic inside controller
public function store(Request $request)
{
Subscriber::create(['email' => $request->email]);
Mail::to($request->email)->send(new ConfirmationMail());
// ... more logic
}
5. Use __DIR__ for All Paths
// Good — relative, portable
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
$this->loadViewsFrom(__DIR__ . '/../views', 'newsletter');
// ❌ Bad — absolute path, breaks on different machines
$this->loadMigrationsFrom('/var/www/app/modules/Newsletter/database/migrations');
6. Write Tests for Every Module
tests/
└── Modules/
└── Newsletter/
├── Unit/
│ └── SubscriberModelTest.php
└── Feature/
├── SubscribeFormTest.php
└── UnsubscribeTest.php
7. Provide a Seeder for Demo Data
php artisan db:seed --class=Modules\\Blog\\Database\\Seeders\\BlogSeeder
8. Document Your Module
Every module should have a README.md:
- What it does
- Installation
- Configuration
- Usage examples
- Available blade components / Livewire components
❌ Avoid These
Don't Import Cross-Module Classes Directly
// ❌ Bad — tight coupling between modules
use Modules\User\Models\User;
use Modules\Blog\Models\Post;
class Newsletter extends Model
{
public function sendToAllBlogReaders()
{
User::whereHas('posts')->each(fn($u) => ...);
}
}
Instead, use events or contracts:
// Good — loose coupling via events
event(new NewPostPublished($post));
// In Newsletter module's listener:
class SendNewsletterOnNewPost
{
public function handle(NewPostPublished $event): void
{
// notify subscribers
}
}
Don't Hardcode Config Values
// ❌ Bad
$perPage = 12;
// Good
$perPage = config('blog.posts_per_page', 12);
Don't Skip Validation
Every user-facing input must be validated:
// Always validate
$validated = $request->validate([
'email' => 'required|email|max:255',
]);
Code Quality Checklist
Before shipping a module:
- [ ] Tables are prefixed with module name
- [ ] All views use the module namespace
- [ ] Business logic is in Services, not controllers
- [ ] Tests cover happy paths and edge cases
- [ ] Config values are not hardcoded
- [ ] ServiceProvider uses
__DIR__relative paths - [ ] Module has a README
- [ ] Module is listed in
config/modules.php - [ ] Namespace added to
composer.jsonautoload