nunomaduro/laravel-starter-kit-inertia-react
Official Laravel starter kit by Nuno Maduro pairing Inertia.js with React, TypeScript, Tailwind CSS, and shadcn/ui. It ships with authentication, a polished dashboard layout, and sensible defaults so you can skip boilerplate and start building your next Laravel app fast.
This is not a package you composer require into an existing app — it is an opinionated skeleton. You adopt it by cloning or creating a project from it, then living inside its conventions. That distinction matters day-to-day: nearly every "how do I do X here?" question is answered by looking at a sibling file rather than by reading package docs.
AGENTS.md (and the .ai/ rules it points at) before touching code. It encodes the non-negotiables: Action pattern, declare(strict_types=1), final readonly classes, PHP 8 constructor promotion, explicit return types, TitleCase enum cases.composer run dev
If you only see stale UI, you need npm run dev or npm run build — this is the single most common "my change didn't apply" cause.php artisan test --compact
vendor/bin/pint --test
vendor/bin/phpstan analyse # Larastan
vendor/bin/rector --dry-run
resources/js/pages, and the Wayfinder output directories (@/actions, @/routes). You will not hand-write URLs in this codebase.| Concern | Look here first |
|---|---|
| Business logic | app/Actions |
| Auth flows | app/Actions/Fortify, FortifyServiceProvider, config/fortify.php |
| Pages / screens | resources/js/pages |
| Typed route calls | @/actions, @/routes, vite.config.js (wayfinder plugin) |
| Rules for AI + humans | AGENTS.md, .ai/* |
| Quality gates | composer.json scripts, phpstan.neon, rector.php, pint.json |
The canonical end-to-end slice is: Action → Controller → Wayfinder function → React form.
<?php
declare(strict_types=1);
namespace App\Actions;
final readonly class CreateFavorite
{
public function handle(User $user, string $favorite): bool
{
// DB::transaction() here if multiple models are involved
return $user->favorites()->create(['name' => $favorite]) !== null;
}
}
Scaffold it with php artisan make:action "CreateFavorite" --no-interaction, not by hand — the generator emits the correct final readonly shape and strict types. Then call it from a controller that Inertia::render()s, and consume the route from React via Wayfinder. That one loop exercises every layer of the kit.
Actions are the default home for logic, and they are deliberately context-free: the same class gets called from HTTP controllers, queued jobs, Artisan commands, MCP tools, and API endpoints. Practical consequences:
handle() as the only public entry point. If you feel the urge to add a second public method, you probably have two actions.private promoted constructor properties; skip the constructor entirely when there are none (an empty zero-arg __construct() is explicitly disallowed).DB::transaction() inside the action, not in the controller — otherwise a job calling the same action bypasses the transaction.Action/Service suffix: CreateFavorite, UpdateUserProfileInformation, SendInvoice.This pays off in testing: a unit test against handle() covers every caller at once, which is why the kit can demand "every change must be programmatically tested" without the suite becoming unbearable.
Wayfinder generates typed functions from your Laravel routes, so the frontend breaks at compile time when a route changes, not at runtime.
import { store } from '@/actions/App/Http/Controllers/FavoriteController';
import { index } from '@/routes/favorites';
// link
<a href={index.url()}>Favorites</a>
// form submit
<form onSubmit={store.form({ method: 'post' })}>...</form>
Working rules:
@/actions/@/routes before fetch('/favorites'). Hardcoded URLs are the thing this kit exists to eliminate..url() for hrefs/navigation, .form() for Inertia <Form>/form props, .get()/.post() for programmatic calls.wayfinder:generate (and the Vite plugin during dev) owns those files; treat them like vendor/.The v3 surface is where most day-to-day gains live:
try/catch that restores previous state.useHttp for standalone requests that aren't visits (typeahead, autosave). Note Axios is gone — use the built-in XHR client and interceptors rather than adding Axios back casually.useLayoutProps so persistent layouts (nav, sidebar) receive props without every page re-declaring them.setInterval and manual state merging.Server side stays boring on purpose: a controller method calls an Action and returns Inertia::render('favorites/index', [...]). Prop types (optional(), defer(), merge()) compose into nested arrays via dot-notation, which is handy for wrapping deferred data in a larger payload.
Do not build login/registration by hand. Fortify registers the routes and controllers; you customize at the seams:
app/Actions/Fortify/CreateNewUserUpdateUserProfileInformationLoginResponseconfig/fortify.php + FortifyServiceProviderThe React side consumes these through Wayfinder route functions, so enabling 2FA is a config + UI task, not a routing task.
Feature tests are the default (php artisan make:test, --unit for the rare unit test); use factories and their named states before hand-building models. Run narrow, not wide:
php artisan test --compact tests/Feature/CreateFavoriteTest.php
The kit's philosophy — every change gets a test, and the minimum number of tests runs — only works if you keep tests scoped. Reach for php artisan tinker --execute '...' with single quotes for one-off inspection; don't leave verification scripts behind.
The MCP tooling is not decoration:
search-docs before code changes — it returns version-specific docs for the exact installed majors (Laravel 13, Inertia v3, Fortify v1). Passing a packages array scopes results; queries are auto-stemmed AND, so use "quoted phrases" for adjacency and multiple queries for OR.database-schema before writing a migration or model; database-query instead of raw SQL in tinker.get-absolute-url before you paste a link to a teammate.browser-logs for frontend exceptions — recent entries only.The three domain skills (laravel-best-practices, wayfinder-development, fortify-development) are meant to be activated on entry to the domain, not after you're stuck. In a strict codebase, that early activation is what keeps you from inventing a fourth way to do something.
PHP 8.5 + Pint + Rector + Larastan at level-high means sloppy code doesn't survive composer scripts. Expect:
@param array{id: int, name: string} $data) rather than array.@var/@return where the signature already says it. Only @-prefixed typing annotations that add information.Inertia::lazy() / LazyProp is removed → Inertia::optional().router.cancel() → router.cancelAll().invalid → httpException, exception → networkError.future config namespace is gone; all v2 future options are always on. Any config referencing it is dead weight.window.axios exists.The kit treats a missing skeleton as a defect. If you add Inertia::defer(...), add the pulsing placeholder in the same commit — otherwise reviewers (and the guidelines) will bounce it. Same for optimistic updates: verify the rollback path, since the happy path hides it.
wayfinder:generate or restart npm run dev.Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest means no build and no dev server. npm run build, or start npm run dev/composer run dev. In v3 dev mode, SSR is handled by the @inertiajs/vite plugin automatically — you don't run a separate Node SSR server locally, so don't go looking for one when debugging rendering.
app/Actions — don't create parallel Services/ or Support/ base folders without approval.AGENTS.md; a second source of truth rots.| Need | Extend here |
|---|---|
| New business capability | app/Actions/{Verb}{Noun} via make:action |
| New auth behaviour | Fortify actions + LoginResponse binding |
| New screen | resources/js/pages + Inertia::render() + Wayfinder import |
| New quality gate | composer.json scripts (keep --compact/narrow runs fast) |
| New AI guidance | .ai/ rules consumed by AGENTS.md |
The through-line: this kit trades flexibility for a single obvious way to do each thing. Leverage it by generating rather than hand-writing, importing rather than hardcoding, and testing the action rather than every caller.
How can I help you explore Laravel packages today?