A fully working employee directory that demonstrates Handsontable's dataProvider plugin connected to a Django REST Framework backend. All pagination, sorting, filtering, and CRUD operations are handled server-side.
| Layer | Tech |
|---|---|
| Database | PostgreSQL 15 (Docker) |
| Backend | Python 3.11, Django 4, Django REST Framework |
| Frontend (JS) | Vite, Handsontable (dataProvider plugin) |
| Frontend (Angular) | Angular 21, @handsontable/angular-wrapper |
| Frontend (React) | React 19, @handsontable/react-wrapper |
Prerequisites: Docker (with Compose plugin), Node.js, npm
bash setup.shOr with Make:
make setupThe script will:
- Build and start PostgreSQL + Django via Docker Compose
- Run database migrations inside the container
- Seed 50 realistic employee records
- Install all frontend npm dependencies (JS, Angular, React)
- Build Angular and React apps, then start watchers for live rebuilds
- Start the Vite dev server
Open http://localhost:5173 in your browser.
| URL | Description |
|---|---|
| http://localhost:5173 | JS frontend |
| http://localhost:5173/angular.html | Angular frontend |
| http://localhost:5173/react.html | React frontend |
make setup # Full first-time setup (build → migrate → seed → start)
make backend # Start only the Docker services
make frontend # Install deps and start Vite
make stop # Stop Docker services
make clean # Remove containers, volumes, and node_modulesserver-examples/django/
├── setup.sh # One-run setup script
├── Makefile # Convenience targets
├── docker-compose.yml # PostgreSQL + Django services
├── backend/
│ ├── Dockerfile
│ ├── entrypoint.sh # Wait for DB → migrate → seed → runserver
│ ├── requirements.txt
│ ├── manage.py
│ ├── myproject/ # Django project settings and root URLs
│ └── employees/ # Django app
│ ├── models.py # Employee model (DecimalField salary)
│ ├── serializers.py
│ ├── pagination.py # Maps { count, results } → { rows, totalRows }
│ ├── views.py # Sort/filter translation + batch CRUD endpoints
│ ├── urls.py
│ └── management/commands/seed.py
├── frontend/ # JS entry point + Vite dev server (serves all 3 variants)
│ ├── package.json
│ ├── vite.config.js # Proxies /api/* → Django; serves Angular/React builds
│ ├── favicon.png
│ ├── index.html
│ └── src/main.js # Handsontable + dataProvider setup
├── frontend-angular/ # Angular variant (ng build --watch → served via Vite)
│ ├── angular.json
│ ├── package.json
│ └── src/app/
│ ├── app.component.ts
│ └── app.component.html
└── frontend-react/ # React variant (vite build --watch → served via Vite)
├── vite.config.ts
├── package.json
└── src/
├── main.tsx
└── App.tsx
| Method | URL | Description |
|---|---|---|
GET |
/api/employees/ |
Paginated list with sort and filter support |
POST |
/api/employees/create-rows/ |
Batch create |
PATCH |
/api/employees/update-rows/ |
Batch partial update |
DELETE |
/api/employees/remove-rows/ |
Batch delete |
| Parameter | Example | Description |
|---|---|---|
page |
1 |
1-based page index |
pageSize |
10 |
Rows per page (max 100) |
sort[prop] |
salary |
Column data key to sort by |
sort[order] |
desc |
asc or desc |
filters |
[{"prop":"department","operation":"conjunction","conditions":[{"name":"contains","args":["Eng"]}]}] |
JSON-encoded filter array |
EmployeePagination overrides DRF's default { count, results } response shape to return { rows, totalRows } — the shape dataProvider expects — so the fetchRows callback can return res.json() directly.
Handsontable sends sort[prop]=salary&sort[order]=desc. The Django view reads these, validates the field against an allowlist, and calls queryset.order_by('-salary').
dataProvider passes filters as a JSON array of column descriptors:
[{ "prop": "department", "operation": "conjunction", "conditions": [{ "name": "contains", "args": ["Engineering"] }] }]The frontend serializes this as ?filters=<JSON>. The Django view parses it, maps condition names (contains, eq, begins_with, gte, …) to Django ORM lookups, and combines them into Q objects respecting conjunction/disjunction per column.
The Vite dev server proxies /api/* to Django (vite.config.js), so the browser sees a single origin. Django's csrftoken cookie is readable by JavaScript, and getCsrfToken() forwards it as X-CSRFToken on every mutating request.
dataProvider sends all row mutations as arrays in a single request. Three DRF @action endpoints handle this without requiring individual REST calls per row:
create-rows/—many=Trueserializer, returns created rows with server-assigned IDsupdate-rows/—partial=Trueper row, updates only changed fieldsremove-rows/—filter(pk__in=ids).delete()in one SQL statement
# View backend logs
docker compose logs -f backend
# Stop all services
make stop # or: docker compose down
# Reset database (removes all data)
make clean # or: docker compose down -v
# Restart just the backend after code changes
docker compose up --build -d backend