Adding a Module
1. Generate the module scaffold
php artisan module:make YourModuleThis creates Modules/YourModule/ with the standard directory structure including providers, routes, config, database directories, and test directories.
2. Verify module activation
Check modules_statuses.json in the project root. The new module should be automatically added:
{
"YourModule": true
}3. Configure the RouteServiceProvider
Edit Modules/YourModule/app/Providers/RouteServiceProvider.php to set your API route prefix:
<?php
namespace Modules\YourModule\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
protected string $name = 'YourModule';
public function boot(): void
{
parent::boot();
}
public function map(): void
{
$this->mapApiRoutes();
}
protected function mapApiRoutes(): void
{
Route::prefix('your-module')
->name('your-module.')
->middleware(SubstituteBindings::class)
->group(module_path($this->name, '/routes/api.php'));
}
}Note on prefix conventions: Some modules prefix with just the module name (e.g., properties, events), while others use api/ prefix (e.g., Bills uses api/bills). Choose based on whether the module is consumed by the mobile app (no api/ prefix) or the web app (api/ prefix). If adding CORS, include CorsMiddleware::class in the middleware array.
4. Define routes
Edit Modules/YourModule/routes/api.php:
<?php
use Illuminate\Support\Facades\Route;
use Modules\YourModule\Http\Controllers\YourController;
// Public routes
Route::get('/', [YourController::class, 'index']);
// Authenticated routes
Route::middleware('auth:legacy-jwt')->group(function () {
Route::post('/', [YourController::class, 'store']);
});5. Create a model
php artisan module:make-model Item YourModulePlace models in Modules/YourModule/app/Models/. Follow existing patterns:
<?php
namespace Modules\YourModule\Models;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
protected $fillable = ['name', 'description'];
}6. Create a migration
php artisan module:make-migration create_items_table YourModuleMigrations go in Modules/YourModule/database/migrations/. They are auto-discovered (configured in config/modules.php under auto-discover.migrations).
7. Create a FormRequest
php artisan module:make-request StoreItemRequest YourModule<?php
namespace Modules\YourModule\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreItemRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Or use a policy
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'description' => 'nullable|string',
];
}
}8. Create a Service
Create Modules/YourModule/app/Services/ItemService.php manually (no artisan generator):
<?php
namespace Modules\YourModule\Services;
use Modules\YourModule\Models\Item;
class ItemService
{
public function create(array $data): Item
{
return Item::create($data);
}
}9. Create an API Resource
<?php
namespace Modules\YourModule\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ItemResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'created_at' => $this->created_at,
];
}
}10. Create a Controller
php artisan module:make-controller ItemController YourModuleWire everything together:
<?php
namespace Modules\YourModule\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Modules\YourModule\Http\Requests\StoreItemRequest;
use Modules\YourModule\Http\Resources\ItemResource;
use Modules\YourModule\Services\ItemService;
class ItemController extends Controller
{
public function __construct(
private ItemService $itemService
) {}
public function index(): JsonResponse
{
$items = Item::all();
return ItemResource::collection($items)->response();
}
public function store(StoreItemRequest $request): JsonResponse
{
$item = $this->itemService->create($request->validated());
return ItemResource::make($item)->response()->setStatusCode(201);
}
}11. Register the module in phpunit.xml
If the module has tests, add the test directories to phpunit.xml:
<testsuite name="Feature">
<!-- existing directories -->
<directory>Modules/YourModule/tests/Feature</directory>
</testsuite>12. Write tests
See testing.md for patterns. Create tests in Modules/YourModule/tests/Feature/:
<?php
uses(Tests\FeatureTestCase::class);
test('it lists items', function () {
$response = $this->getJson('/your-module');
$response->assertStatus(200);
});Checklist
- Module generated and activated in
modules_statuses.json -
RouteServiceProviderconfigured with correct prefix - Routes defined in
routes/api.php - Models with proper
$fillableand relationships - Migrations created
- FormRequests for input validation
- Service layer for business logic
- API Resources for JSON output
- Controller wiring everything together
- Tests registered in
phpunit.xml - Tests written and passing