Create the Project
The scaffold is stock Laravel + Breeze, then a small, fixed set of packages layered on top. Nothing here is app-specific — it's the same starting point every time.
1. Scaffold Laravel + Breeze
laravel new my-app
cd my-app
# Breeze with the React + Inertia + TypeScript stack
composer require laravel/breeze --dev
php artisan breeze:install react --typescript
breeze:install react --typescript gives you Inertia v2, @inertiajs/react, a Vite + React + TS frontend, Tailwind, and the full auth flow (login, register, password reset, email verification, profile) as .tsx pages under resources/js/Pages/Auth.
Tip — Add SSR later with
--ssrif you need it. It's off by default here to keep shared-hosting deploys simple.
2. Add the core packages
Composer (runtime):
composer require inertiajs/inertia-laravel tightenco/ziggy \
spatie/laravel-data spatie/laravel-typescript-transformer
Composer (dev / tooling):
composer require --dev laravel/boost laravel/pail laravel/pint
npm (frontend + linting):
npm install
npm install -D eslint prettier \
eslint-config-airbnb eslint-config-airbnb-typescript \
@typescript-eslint/eslint-parser @typescript-eslint/eslint-plugin \
eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y \
eslint-plugin-import eslint-config-prettier
| Package | Role |
|---|---|
inertiajs/inertia-laravel + @inertiajs/react |
Server-driven SPA transport |
tightenco/ziggy |
Use named Laravel routes in JS (route('name')) |
spatie/laravel-data |
DTOs — the typed Inertia payload |
spatie/laravel-typescript-transformer |
Generates .d.ts from those DTOs |
laravel/boost |
MCP server + AI guidelines for coding agents |
laravel/pail |
Tail application logs from the terminal |
laravel/pint |
PHP code style (Laravel preset) |
| ESLint (Airbnb) + Prettier | JS/TS quality gate — see Frontend |
Tailwind v4 setup and the Airbnb ESLint config are covered on Frontend — React & Tailwind. DTO wiring is on DTOs & TypeScript. Boost is on AI-driven dev docs.
3. Directory layout
Two conventions do most of the work: backend layering in app/, and a casing split in resources/js/.
app/
├── Console/Commands/ # RemoteCmds lives here (see page 07)
├── Data/ # Spatie DTOs (#[TypeScript])
├── Enums/ # PHP enums (also transformed to TS)
├── Http/
│ ├── Controllers/ # Thin orchestrators
│ └── Requests/ # FormRequest per write action
├── Models/ # Lightweight Eloquent models
├── Providers/ # incl. TypeScriptTransformerServiceProvider
└── Services/ # Business logic
resources/js/
├── app.tsx # Inertia entry
├── bootstrap.ts # axios + Ziggy
├── Pages/ # Breeze + app pages ← Capitalized (scaffold)
├── Components/ # Breeze UI primitives ← Capitalized (scaffold)
├── Layouts/ # Auth/Guest layouts ← Capitalized (scaffold)
├── features/ # App feature modules ← lowercase (your code)
├── shared/ # Cross-feature code ← lowercase (your code)
└── types/ # generated.d.ts, index.d.ts, global.d.ts
Tip — The capitalized
Pages/Components/Layoutsare Breeze scaffolding — leave them recognizable. Lowercasefeatures/andshared/are where your application code goes. The casing itself signals "framework vs app" at a glance.
Mirror feature names across the boundary. As the app grows, group within the standard app/ dirs by the same feature nouns the frontend uses — one feature, one greppable name on both sides:
app/Data/Rooms/RoomStateData.php resources/js/features/rooms/
app/Services/Rooms/RoomService.php
app/Http/Requests/Rooms/CreateRoomRequest.php
This is the proportional step for medium apps — deliberately not full DDD app/Domain/ modules, which are overkill until a project genuinely demands them.
4. Local site & dev commands
Every project runs at sitename.localhost (add it to the Apache/hosts config once) and PHP serves on port 80, so the local URL mirrors production's shape. My dev-tools suite handles the XAMPP side of this — vhost/hosts wiring and local-site setup — so a new sitename.localhost is a one-command job:
php artisan serve --host=my-app.localhost --port=80
The daily driver is npm run hot — Vite HMR plus the browser opening itself, via concurrently:
// package.json scripts
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"hot": "concurrently \"npm run dev\" \"npm run open\"",
"hot:fresh": "php artisan migrate:fresh --seed",
"hot:all": "npm run hot:fresh && npm run hot",
"serve:dev": "php artisan serve --host=my-app.localhost --port=80",
"open": "start chrome http://my-app.localhost",
"lint": "eslint resources/js --ext .ts,.tsx",
"lint:fix": "eslint resources/js --ext .ts,.tsx --fix",
"format": "prettier --write resources/js"
}
}
| Script | Use |
|---|---|
npm run hot |
The everyday loop — Vite HMR + Chrome opens the site directly |
npm run hot:fresh |
Rebuild the DB (migrate:fresh --seed) |
npm run hot:all |
Fresh DB, then straight into the hot loop |
# Regenerate TypeScript types after touching a DTO or Enum
php artisan typescript:transform
# Quality gates
vendor/bin/pint --dirty # PHP style
npm run lint # ESLint (Airbnb)
npm run format # Prettier
npx tsc # Type check
Tip — Keep Vite's dev server pinned in
vite.config.ts(server: { host: '127.0.0.1', port: 5173, hmr: { host: 'localhost', port: 5173 } }) so HMR works reliably when the site itself runs onmy-app.localhost. If Vite ever crashes it can leave a stalepublic/hotfile behind — delete it and assets serve from the last build again.