Handsontable — Server-Side Data Management with Express.js
The example wires a Handsontable grid to an Express.js REST API backed by PostgreSQL. All data operations — pagination, sorting, filtering, and CRUD — are handled server-side. The grid is available in three frontend variants: vanilla JS, Angular, and React.
Layer
Technology
Frontend
Handsontable — vanilla JS, Angular, and React
Backend
Express.js 4 + TypeScript + Zod
ORM
TypeORM 0.3
Database
PostgreSQL 16 (Docker)
Docker (with Compose v2)
Node.js 18+
npm
bash setup.sh
# or: make setup
That single command:
Starts a PostgreSQL 16 container via Docker Compose
Waits for PostgreSQL to be ready
Installs Express server dependencies (npm install)
Runs TypeORM migrations — creates the tickets table and seeds 12 sample rows
Starts the Express backend on http://localhost:3000
Installs Vite client dependencies
Installs, builds, and starts Angular + React watchers in the background
Opens the Vite dev server on http://localhost:5173
URL
Description
http://localhost:5173/
Vanilla JS
http://localhost:5173/angular.html
Angular
http://localhost:5173/react.html
React
Press Ctrl+C to stop everything.
make setup # Full first-time setup (start DB → migrate → seed → backend → frontend)
make start # Start DB + backend + frontend after the initial setup (skips migrations)
make stop # Stop the DB container (preserves data)
make logs # Stream database container logs
make clean # Remove containers, volumes, and node_modules
make reset # Full clean then setup (clean restart)
make psql # Open a psql session in the running PostgreSQL container
express/
├── setup.sh # One-command bootstrap script
├── Makefile # make setup / start / stop / logs / clean / reset / psql
├── docker-compose.yml # PostgreSQL 16 service
│
├── server/ # Express.js backend
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
│ ├── main.ts # App bootstrap + CORS + JSON body parser
│ ├── data-source.ts # TypeORM DataSource (used by CLI)
│ ├── ticket.entity.ts # TypeORM entity (UUID PK)
│ ├── types.ts # Zod validation schema + FetchTicketsParams
│ ├── tickets.router.ts # GET / POST / PATCH / DELETE /tickets
│ ├── tickets.service.ts # Filtering, sorting, pagination, CRUD
│ └── migrations/
│ └── 1700000000000-CreateTickets.ts # Schema + seed data
│
├── client/ # Vite dev server — entry point for all variants
│ ├── package.json
│ ├── vite.config.js # Proxies /tickets; serves Angular & React builds via middleware
│ ├── index.html
│ └── src/
│ └── main.js # Handsontable dataProvider configuration — vanilla JS
│
├── client-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 + <hot-table>
│
└── client-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/useState)
└── styles.css
Method
Path
Description
GET
/tickets
Paginated, sorted, filtered list
POST
/tickets
Create one or more tickets
PATCH
/tickets
Batch update tickets
DELETE
/tickets
Batch delete tickets
GET /tickets — query parameters
Parameter
Example
Description
page
1
1-based page number
pageSize
5
Rows per page
sort[column]
status
Column to sort by
sort[order]
asc | desc
Sort direction
filters[0][prop]
status
Column property name
filters[0][condition]
eq
Filter condition
filters[0][value][0]
open
Filter value
Filter conditions: eq, neq, contains, not_contains, begins_with, ends_with, empty, not_empty
{
"rows" : [{ "id" : " uuid" , "subject" : " ..." , "status" : " open" , "priority" : " high" , "assignee" : " ..." , "createdAt" : " 2025-01-15" }],
"totalRows" : 12
}
Handsontable features used
Feature
Option
Server-side data
dataProvider (fetchRows, onRowsCreate, onRowsUpdate, onRowsRemove)
Pagination
pagination: { pageSize: 5 }
Column sorting
columnSorting: true
Filtering
filters: true, dropdownMenu: true
Loading / empty state
emptyDataState: true
Error toasts
notification: true