A fully runnable implementation of the Server-side data with Laravel recipe.
A product inventory data grid available in three variants:
REST API (/)
- Fetches paginated rows from
GET /api/productson every page change - Sorts and filters rows on the server — the browser never loads the full dataset
- Creates, updates, and deletes rows via
POST,PATCH, andDELETEendpoints
Angular (/angular.html)
- Same REST API operations wrapped in an Angular standalone component
- Uses
@handsontable/angular-wrapper(HotTableComponent) with@ViewChildfor instance access - Built separately with
ng build --watchand served through the Vite dev server via a custom middleware plugin
React (/react.html)
- Same REST API operations wrapped in a React functional component
- Uses
@handsontable/react-wrapper(HotTable) withuseReffor instance access anduseMemofor stable settings - Built separately with
vite build --watchand served through the Vite dev server via a custom middleware plugin
| Tool | Version |
|---|---|
| Docker + Docker Compose (v2 plugin or v1 standalone) | any recent |
| Node.js | 18+ |
| npm | 9+ |
bash setup.sh
# or: make setupThat single command:
- Builds a PHP 8.2 / Apache Docker image with a fresh Laravel 11 project
- Starts a MySQL 8 container (health-checked before the app starts)
- Runs database migrations and seeds 52 sample products
- Installs frontend dependencies (Angular and React are pre-built before Vite starts)
- Opens http://localhost:5173
| URL | Description |
|---|---|
http://localhost:5173/ |
REST API (vanilla JS) |
http://localhost:5173/angular.html |
Angular |
http://localhost:5173/react.html |
React |
Switch between variants using the nav links at the top of each page.
make setup # Full first-time setup (build → migrate → seed → start)
make start # Start Docker + frontend after the initial setup
make stop # Stop Docker services
make logs # Stream Laravel container logs
make clean # Remove containers, volumes, and node_modulesserver-side-laravel/
├── setup.sh # One-run setup script
├── Makefile # Convenience targets
├── docker-compose.yml # MySQL + Laravel services
│
├── laravel/ # Laravel backend (Dockerised)
│ ├── Dockerfile # PHP 8.2 / Apache image
│ ├── apache.conf # VirtualHost pointing at public/
│ ├── entrypoint.sh # migrate → seed → apache2-foreground
│ ├── bootstrap/app.php # Registers routes/api.php (Laravel 11)
│ ├── app/
│ │ ├── Http/Controllers/ProductController.php
│ │ └── Models/Product.php
│ ├── database/
│ │ ├── migrations/ # Products table schema
│ │ └── seeders/ # 52 sample products
│ └── routes/api.php # GET / POST / PATCH / DELETE /api/products
│
├── frontend/ # Vite dev server — entry point for all variants
│ ├── package.json
│ ├── vite.config.js # Proxies /api; serves Angular & React builds via middleware
│ ├── index.html
│ └── src/main.js # dataProvider configuration — REST
│
├── frontend-angular/ # Angular standalone app (ng build --watch)
│ ├── angular.json # outputPath.base=dist, baseHref=/angular-assets/
│ ├── package.json
│ └── src/app/
│ ├── app.component.ts # HotTableComponent + dataProvider logic
│ └── app.component.html # template with nav + toolbar + <hot-table>
│
└── frontend-react/ # React app (vite build --watch)
├── vite.config.ts # base=/react-assets/, input=react.html
├── package.json
├── react.html # HTML entry point
└── src/
├── main.tsx # createRoot bootstrap
├── App.tsx # HotTable + dataProvider logic (useRef/useMemo)
└── styles.css
Browser (Vite :5173)
│ GET /api/products?page=1&pageSize=10&sort[prop]=price&sort[order]=asc
▼
Vite proxy → Laravel (Docker :8000)
│ ProductController::index()
│ filter loop (allowlisted columns, LOWER() for case-insensitive text)
│ orderBy (allowlisted columns)
│ skip/take pagination
▼
MySQL 8 ← → Eloquent ORM
│
▼
{ data: [...10 rows...], total: 52 }
│
▼
Handsontable dataProvider → renders page 1, shows pagination bar
buildUrl() serialises the queryParameters object that fetchRows receives into bracket-notation query params that Laravel parses automatically with request()->input():
filters[0][prop]=price&filters[0][condition]=gt&filters[0][value]=100
The Vite proxy forwards every /api request to http://localhost:8000, so no CORS configuration is required.
| HTTP method | Handsontable callback | Action |
|---|---|---|
GET |
fetchRows |
Paginate, sort, filter |
POST |
onRowsCreate |
Insert blank rows |
PATCH |
onRowsUpdate |
Batch-update changed cells |
DELETE |
onRowsRemove |
Delete rows by ID |
Both orderBy and whereRaw validate the column name against an allowlist before use, preventing SQL injection through user-supplied sort/filter parameters.
Press Ctrl+C in the terminal to stop the Vite dev server, then run:
make stop
# or: docker compose down