sylpi10 / symfony-inertia-react-starter
Symfony starter with React + TypeScript via Inertia.js, with SSR
Package info
github.com/sylpi10/symfony-inertia-react-starter
Type:project
pkg:composer/sylpi10/symfony-inertia-react-starter
Requires
- php: >=8.4
- ext-ctype: *
- ext-iconv: *
- ext-pdo_sqlite: *
- doctrine/doctrine-bundle: ^3.3
- doctrine/doctrine-migrations-bundle: ^4.0
- doctrine/orm: ^3.7
- nytodev/inertia-bundle: ^3.1
- pentatrion/vite-bundle: ^8.2
- phpdocumentor/reflection-docblock: ^6.0
- phpstan/phpdoc-parser: ^2.3
- symfony/console: 8.1.*
- symfony/dotenv: 8.1.*
- symfony/flex: ^2
- symfony/framework-bundle: 8.1.*
- symfony/http-client: 8.1.*
- symfony/mailer: 8.1.*
- symfony/property-access: 8.1.*
- symfony/property-info: 8.1.*
- symfony/rate-limiter: 8.1.*
- symfony/runtime: 8.1.*
- symfony/security-bundle: 8.1.*
- symfony/serializer: 8.1.*
- symfony/twig-bundle: 8.1.*
- symfony/validator: 8.1.*
- symfony/yaml: 8.1.*
- symfonycasts/reset-password-bundle: ^1.25
- symfonycasts/verify-email-bundle: ^1.18
- twig/extra-bundle: ^2.12|^3.0
- twig/twig: ^2.12|^3.30
Requires (Dev)
- dama/doctrine-test-bundle: ^8.7
- doctrine/doctrine-fixtures-bundle: ^4.3
- phpunit/phpunit: ^13.3.6
- spatie/typescript-transformer: ^3.3
- symfony/browser-kit: 8.1.*
- symfony/css-selector: 8.1.*
- symfony/debug-bundle: 8.1.*
- symfony/maker-bundle: ^1.68
- symfony/monolog-bundle: ^3.0|^4.0
- symfony/stopwatch: 8.1.*
- symfony/web-profiler-bundle: 8.1.*
Suggests
None
Provides
None
Conflicts
This package is auto-updated.
Last update: 2026-10-12 17:15:05 UTC
README
A Symfony 8.1 starter with a React 19 + TypeScript front end, glued together by Inertia.js v3: you build a single-page app without writing an API. Controllers stay regular Symfony controllers, but instead of rendering Twig templates they return a React page + its props.
Included: Vite, Sass, server-side rendering, authentication (login, registration, password reset, email verification), CSRF protection, an example admin CRUD, TypeScript types generated from PHP DTOs, functional tests, a Docker dev stack and a GitHub Actions CI.
Requirements
- PHP ≥ 8.4 with
pdo_sqlite - Composer, Node.js (v24) + npm
- Symfony CLI (recommended, required by
make dev) make(optional)
Or only Docker: see Running with Docker.
Getting started
composer create-project sylpi10/symfony-inertia-react-starter my-app
cd my-app
npm install
create-project installs the PHP dependencies, then runs composer setup, which:
- generates a fresh
APP_SECRETin.env.dev(so that no two projects share the same one); - runs the migrations, creating
var/data_dev.db(SQLite); - loads the fixtures: demo data + demo accounts;
- prepares the test database (
var/data_test.db) the same way, so thatphp bin/phpunitworks right away.
Then make the project yours: replace name and description in composer.json (they still read sylpi10/symfony-inertia-react-starter), and run composer update --lock to keep composer.lock in sync.
Prefer GitHub's Use this template button? Clone your new repository, then run the same steps by hand:
composer install composer setup npm install
⚠️
composer setupis meant to run once: it resets both databases (dev and test) and replacesAPP_SECRET.
Running in development
make dev
This starts the Symfony server in the background, along with the Vite dev server (hot reload), declared as a worker in .symfony.local.yaml.
| Command | What it does |
|---|---|
make dev |
Starts the Symfony server + Vite in the background |
make stop |
Stops the server and its workers |
make logs |
Tails the server and Vite logs |
make build |
Builds the client and SSR bundles (public/build, bootstrap/ssr) |
make test |
Prepares the test database, then runs PHPUnit |
Without make: symfony serve -d does the same as make dev. Without the Symfony CLI, run your PHP server and npm run dev in two terminals.
Open http://localhost:8000 and sign in with one of the demo accounts:
| Account | Password | Role |
|---|---|---|
test@example.com |
test1234 |
Regular user |
admin@example.com |
admin1234 |
ROLE_ADMIN: gets the Admin nav link |
⚠️
localhost:5173is Vite's asset server, not the app. Always browse through Symfony (port 8000).
Useful commands:
| Command | What it does |
|---|---|
composer types |
Regenerates assets/types/generated.ts from PHP DTOs |
npm run typecheck |
Type-checks the front end (tsc --noEmit) |
npm run lint |
Checks the formatting of assets/ with Prettier |
symfony console doctrine:fixtures:load -n |
Resets the database with demo data (deletes all users!) |
/_profiler |
Symfony profiler: requests, SQL, emails sent in dev |
SSR is off in development (no hot reload on the Node side). To check it: see SSR and production.
Running with Docker
A development stack that needs neither PHP nor Node on your machine: FrankenPHP for Symfony, and a Node container for Vite (hot reload included).
make docker-up # first start takes a minute or two (composer install, npm ci) make docker-fixtures # demo data + demo accounts
Then open http://localhost:8000, as without Docker.
| Command | What it does |
|---|---|
make docker-up |
Builds and starts the containers |
make docker-down |
Stops them (volumes are kept) |
make docker-logs |
Follows the logs of both containers |
make docker-fixtures |
Loads the fixtures in the Docker database |
make docker-test |
Runs PHPUnit in the php container |
Good to know:
- The
phpcontainer runscomposer installand the migrations each time it starts. - The SQLite database lives in a Docker volume, separate from
var/data_dev.db.docker compose down -vdeletes it, along with the container'snode_modules. - Containers run as your user, so
vendor/andvar/stay yours. - Ports 8000 and 5173 are the same as
make dev: stop one before starting the other, or change them withAPP_PORT=8080 VITE_PORT=5174 make docker-up. - This stack is for development only. For production, see SSR and production.
Adding a page
1. A props DTO, marked #[TypeScript]:
// src/Dto/Page/ContactPageProps.php #[TypeScript] final readonly class ContactPageProps { public function __construct( public string $email, ) { } }
2. The controller returns a page name + the DTO:
// src/Controller/ContactController.php #[Route('/contact', name: 'app_contact')] public function index(Inertia $inertia): Response { return $inertia->render('Contact', get_object_vars(new ContactPageProps( email: 'hello@example.com', ))); }
3. Generate the types: composer types
4. The page component, in assets/Pages/ (the name passed to render() is the file path):
// assets/Pages/Contact.tsx import type { ContactPageProps } from "../types/generated"; export default function Contact({ email }: ContactPageProps) { return <h1>Write to {email}</h1>; }
No front-end route, no API call. Links between pages use <Link href="/contact"> from @inertiajs/react.
Example admin CRUD
/admin/technos manages the technos listed on the home page. It is a reference for the patterns you will reuse in your own CRUDs:
| Pattern | Where to look |
|---|---|
| Access control | #[IsGranted("ROLE_ADMIN")] on Admin/TechnoController: anonymous → /login, non-admin → 403 error page |
| One input DTO, two uses | TechnoInput with #[MapRequestPayload], shared by create (POST) and update (PUT) |
| One form page, two uses | Pages/Admin/Technos/Form.tsx: form.post() or form.put() depending on the techno prop |
| Delete | router.delete() after a confirm(), answered by a 303 redirect |
| Pagination | Doctrine Paginator in TechnoRepository, ?page=N bound with #[MapQueryParameter] |
| Optimistic update | Reordering with router.optimistic(...).patch(...): the row moves at once, Inertia rolls back on failure |
| Enum shared with TS | MoveDirection (up / down), exported to generated.ts |
Conventions
- Never pass a Doctrine entity to
render(): always a DTO (an entity would leak every getter, including password hashes). - Forms: an input DTO with
#[MapRequestPayload]+ Validator constraints on the PHP side,useFormon the React side. Validation errors come back automatically inform.errors. - After a successful POST, PUT, PATCH or DELETE: flash message + redirect with a 303 (with a 302, the browser would replay the redirect with the same method).
- Static UI text goes in the component; props are for data only the server knows (database, user, computed values).
- Twig is only used for the root view (
templates/base.html.twig) and emails.
Project layout (short)
assets/
├── Pages/ # One file = one page rendered by $inertia->render()
├── Layouts/ # AppLayout (default), GuestLayout (Auth/* pages)
├── Components/ # Shared components
├── images/ # UI images, imported by components
├── types/ # generated.ts (never edit) + inertia.d.ts
└── styles/ # Sass
src/
├── Controller/ # Controllers → $inertia->render(), Admin/ for the CRUD
├── Dto/ # Input DTOs, output DTOs, Page/ props
├── Enum/ # Enums, exported to TypeScript
├── Entity/, Repository/, DataFixtures/
└── EventListener/ # Shared props, CSRF, error pages
tests/ # Functional tests (InertiaWebTestCase + Controller/)
Testing
make test # migrations + fixtures on var/data_test.db, then PHPUnit php bin/phpunit --filter testCreate # a single test, once the test database exists
The test database is created by composer setup (or make test). If PHPUnit fails with no such table, it is missing: run make test. Run it again after adding a migration or changing the fixtures.
Functional tests live in tests/Controller/ and extend tests/InertiaWebTestCase.php, which talks to the app the way the Inertia client does:
visit()sends theX-Inertiaheader and copies theXSRF-TOKENcookie intoX-XSRF-TOKEN(without it, every POST is rejected). Responses are JSON, so tests need neither Vite nor a build.page()returns anAssertableInertiaPageto assert the component and its props;flash()reads flash messages.- Fixtures are loaded once; dama/doctrine-test-bundle rolls back the database after each test.
Covered: pages and 404, CSRF, registration, login (case-insensitive email, throttling), and the admin CRUD including access control.
Continuous integration
.github/workflows/ci.yaml runs on every push to master and on pull requests, without Docker:
- PHP:
composer validate,lint:container, migrations +doctrine:schema:validate, PHPUnit, and a check thatcomposer typesleavesassets/typesunchanged (a DTO changed without regenerating the types fails the build). - Front:
npm run typecheck,npm run lint, and the client + SSR builds.
Documentation
| Guide | Contents |
|---|---|
| Architecture | How Inertia works, page resolution, layouts, styles, images, full project structure |
| TypeScript types | DTOs, #[TypeScript], composer types, shared props typing |
| Authentication, forms and security | Login, registration, password reset, email verification, shared props, flash messages, CSRF, error pages, emails |
| SSR and production | Enabling SSR, Node server, production build and environment variables |
| Troubleshooting | Common errors and their fixes |