Module Structure
Understanding how modules are organized helps you build maintainable and consistent integrations.
Directory Layout
A complete module contains backend (Laravel) and frontend (React/Next.js) code:
modules/YourModule/
├── backend/ # Laravel Backend
│ ├── App/
│ │ ├── Http/
│ │ │ ├── Controllers/ # API controllers
│ │ │ └── Requests/ # Form requests
│ │ ├── Models/ # Eloquent models
│ │ ├── Services/ # Business logic
│ │ ├── Jobs/ # Queue jobs
│ │ └── Resources/ # API resources
│ ├── config/ # Module configuration
│ ├── routes/
│ │ ├── api.php # API routes
│ │ └── webhooks.php # Webhook routes
│ ├── Database/
│ │ └── Migrations/ # Database migrations
│ ├── tests/
│ │ ├── Feature/ # API/integration tests
│ │ ├── Unit/ # Unit tests
│ │ └── Pest.php # Test configuration
│ └── YourModuleServiceProvider.php
│
├── frontend/ # React/Next.js Frontend
│ ├── index.ts # Public exports
│ ├── components/ # React components
│ ├── pages/ # Module pages
│ ├── hooks/ # React hooks
│ ├── types/ # TypeScript types
│ └── lib/ # Utilities
│
├── module.json # Module manifest (REQUIRED)
└── README.md
Service Provider
The main entry point for your module. Registers services, config, migrations, and event listeners:
namespace Modules\YourModule;
use Illuminate\Support\ServiceProvider;
class YourModuleServiceProvider extends ServiceProvider
{
protected $moduleName = 'YourModule';
protected $moduleNameLower = 'yourmodule';
public function boot()
{
$this->mergeConfigFrom(
module_path($this->moduleName, 'config/config.php'),
$this->moduleNameLower
);
$this->loadMigrationsFrom(
module_path($this->moduleName, 'Database/Migrations')
);
$this->registerEvents();
}
public function register()
{
$this->app->register(RouteServiceProvider::class);
$this->app->singleton(YourService::class, function ($app) {
return new YourService();
});
}
protected function registerEvents()
{
Event::listen(OrderCreated::class, HandleOrderCreated::class);
}
}
Routes
Define HTTP endpoints in routes/api.php:
use Illuminate\Support\Facades\Route;
use Modules\YourModule\App\Http\Controllers\YourController;
// Authenticated tenant routes
Route::group([
'prefix' => 'integrations/yourmodule',
'middleware' => ['auth:api', 'tenant']
], function () {
Route::post('/connect', [YourController::class, 'connect']);
Route::post('/disconnect', [YourController::class, 'disconnect']);
Route::get('/status', [YourController::class, 'status']);
Route::post('/sync', [YourController::class, 'sync']);
});
// Webhook routes (no auth, tenant resolved from payload)
Route::group([
'prefix' => 'webhooks/yourmodule',
'middleware' => ['tenant']
], function () {
Route::post('/{topic}', [YourController::class, 'webhook']);
});
Models
Eloquent models for module-specific data in App/Models/:
namespace Modules\StoreShopify\App\Models;
use Illuminate\Database\Eloquent\Model;
class ShopifyConnection extends Model
{
protected $fillable = [
'tenant_id', 'shop_domain', 'access_token',
'is_active', 'last_sync_at',
];
protected $casts = [
'is_active' => 'boolean',
'last_sync_at' => 'datetime',
];
}
Services
Business logic and external API integration in App/Services/:
namespace Modules\StoreShopify\App\Services;
class ShopifyService
{
protected $client;
public function __construct()
{
$this->client = new \GuzzleHttp\Client();
}
public function fetchOrders($since = null)
{
$shopDomain = ModuleManager::getSetting('store-shopify', 'shop_domain');
$accessToken = ModuleManager::getSetting('store-shopify', 'access_token');
$response = $this->client->get(
"https://{$shopDomain}/admin/api/2024-01/orders.json",
[
'headers' => ['X-Shopify-Access-Token' => $accessToken],
'query' => ['status' => 'any'],
]
);
return json_decode($response->getBody(), true);
}
}
Queue Jobs
Background processing in App/Jobs/:
namespace Modules\StoreShopify\App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
class SyncShopifyOrders implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
protected string $tenantId,
protected ?Carbon $since = null,
) {}
public function handle(ShopifyService $shopifyService)
{
tenancy()->initialize($this->tenantId);
$orders = $shopifyService->fetchOrders($this->since);
foreach ($orders['orders'] as $orderData) {
OrderService::createFromShopify($orderData);
}
}
}
Events & Listeners
Custom events in App/Events/ and listeners in App/Listeners/:
// App/Events/ShopifyOrderSynced.php
class ShopifyOrderSynced
{
public function __construct(
public Order $order,
public array $shopifyData,
) {}
}
// App/Listeners/NotifyOrderSynced.php
class NotifyOrderSynced
{
public function handle(ShopifyOrderSynced $event)
{
Log::info('Shopify order synced', [
'order_id' => $event->order->id,
'shopify_id' => $event->shopifyData['id'],
]);
}
}
Migrations
Database schema in Database/Migrations/:
return new class extends Migration
{
public function up()
{
Schema::create('shopify_connections', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('shop_domain')->unique();
$table->text('access_token')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamp('last_sync_at')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'shop_domain']);
});
}
public function down()
{
Schema::dropIfExists('shopify_connections');
}
};
File Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Models | StudlyCase |
ShopifyConnection.php |
| Controllers | StudlyCaseController |
ShopifyAuthController.php |
| Services | StudlyCaseService |
ShopifyService.php |
| Jobs | VerbNoun |
SyncShopifyOrders.php |
| Events | NounVerbed |
OrderSynced.php |
| Listeners | VerbNoun |
HandleOrderSynced.php |
Related
- module.json Reference — Complete manifest specification
- Getting Started — Create your first module
- Backend Development — Detailed backend patterns
- Frontend Development — UI components and pages
- Module Testing — Testing your module
- CLI Commands — Generate modules from CLI