Laravel
Laravel 13 + Eloquent + the official React starter kit (Inertia, Fortify login) + Sail (Docker, MySQL 8.4).
Requirements: PHP >= 8.3, Composer, Docker.
1. Create the project
composer create-project laravel/react-starter-kit laravel --stability=dev
cd laravel
This is what laravel new laravel --react runs. --stability=dev matters: the starter
kit is released from its main branch, and the tagged v1.0.1 is an old Laravel 12 kit.
The installer asks which authentication features to keep (email verification, registration, 2FA, passkeys, password confirmation). This build kept the defaults (all). The login screen, session auth, password reset, dashboard and profile pages are now in place — nothing to write for LOGIN.
If PHP runs in a container without Node, prefix the command with
LARAVEL_INSTALLER_NO_NODE=1; the JavaScript packages are installed in step 5.
2. Add Docker (Sail) with MySQL
php artisan sail:install --with=mysql
It writes compose.yaml and points .env to the mysql service. To run next to the
other two projects, set the host ports in .env:
APP_URL=http://localhost:8081
APP_PORT=8081
FORWARD_DB_PORT=3307
3. Generate the Product skeleton
php artisan make:model Product --migration --factory --controller --resource
php artisan make:request ProductRequest
php artisan make:test ProductTest
The files are generated empty: Laravel goes model -> migration -> table, so the columns, the rules, the controller bodies and the pages are written by hand.
4. Fill in the generated files
Migration — database/migrations/*_create_products_table.php
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->decimal('price', 10, 2);
$table->timestamps();
$table->softDeletes();
});
Model — app/Models/Product.php. Eloquent reads the columns from the table, so only
the behaviour is declared:
#[Fillable(['name', 'price'])]
class Product extends Model
{
use HasFactory, SoftDeletes;
}
Validation — app/Http/Requests/ProductRequest.php (remove authorize(); the route
is already behind auth):
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'price' => ['required', 'numeric', 'min:0', 'max:99999999.99'],
];
}
Controller — app/Http/Controllers/ProductController.php (the show method is not
used and can be deleted):
public function index(): Response
{
return Inertia::render('products/index', [
'products' => Product::latest('id')->get(),
]);
}
public function create(): Response
{
return Inertia::render('products/form');
}
public function store(ProductRequest $request): RedirectResponse
{
Product::create($request->validated());
return to_route('products.index');
}
public function edit(Product $product): Response
{
return Inertia::render('products/form', ['product' => $product]);
}
public function update(ProductRequest $request, Product $product): RedirectResponse
{
$product->update($request->validated());
return to_route('products.index');
}
public function destroy(Product $product): RedirectResponse
{
$product->delete();
return to_route('products.index');
}
Route — routes/web.php, inside the existing auth group:
Route::resource('products', ProductController::class)->except('show');
Pages — there is no generator for these; write them by hand:
resources/js/types/product.ts— theProducttype, exported fromtypes/index.tsresources/js/pages/products/index.tsx— the table with Edit / Delete buttonsresources/js/pages/products/form.tsx— one form used for both create and editresources/js/components/app-sidebar.tsx— add theProductsentry tomainNavItems
The typed route helpers the pages import (@/routes/products,
@/actions/App/Http/Controllers/ProductController) are generated by Wayfinder during
the build.
Factory and seeder — database/factories/ProductFactory.php returns
['name' => fake()->words(3, true), 'price' => fake()->randomFloat(2, 1, 1000)], and
database/seeders/DatabaseSeeder.php creates the admin user and five products:
User::factory()->create([
'name' => 'Administrator',
'password' => '!P4ssw0rdstr!',
]);
Product::factory(5)->create();
Tests — tests/Feature/ProductTest.php.
5. Run it
./vendor/bin/sail up -d
./vendor/bin/sail npm install
./vendor/bin/sail npm run build
./vendor/bin/sail artisan migrate:fresh --seed
./vendor/bin/sail test
Open http://localhost:8081, log in with [email protected] / !P4ssw0rdstr! and use
Products in the sidebar. For hot reload while editing pages, run
./vendor/bin/sail npm run dev.
Stop with ./vendor/bin/sail down (add -v to drop the database volume).
Screens and routes
| Method | Path | Result |
|---|---|---|
| GET/POST | /login | login screen (starter kit) |
| GET | /products | list |
| GET/POST | /products/create, /products | new product |
| GET/PUT | /products/{id}/edit, /products/{id} | edit product |
| DELETE | /products/{id} | soft delete (sets deleted_at) |
Guests are redirected to /login on every product route.
What was generated vs written
| Generated by commands | project, login and all auth screens, Docker, the empty skeletons of 6 files, typed route helpers |
| Written by hand | columns in the migration, 2 lines of model behaviour, validation rules, 6 controller bodies, 1 route, 2 React pages + type + menu entry, seeder, tests |
Laravel has no first-party generator that reads an existing table and writes the model,
rules, controller and screens (the Gluo codegen direction). make:model produces
empty skeletons; php artisan model:show Product inspects a model but writes nothing.