A production-ready quick start that connects the Syncfusion® React Pivot Table to a Python FastAPI backend using the UrlAdaptor — enabling remote data binding and full CRUD operations over REST endpoints.
- 🚀 Quick Overview
- ✨ Key Features
- 🛠️ Prerequisites
- 📂 Project Structure
- ⚙️ Installation & Setup
▶️ Running the Application- 🧪 Testing CRUD Operations
- 🔧 Troubleshooting
- 📖 API Reference
- 🤝 Contributing
- 📜 License & Support
- 📚 Related Resources
This project demonstrates how to bind the Syncfusion® React Pivot Table to a remote Python FastAPI backend using the UrlAdaptor of the DataManager. The UrlAdaptor issues POST requests to a single endpoint and routes them to create, read, update, or delete handlers based on an action field in the payload, making it a clean fit for lightweight Python REST services.
| Component | Technology | Purpose |
|---|---|---|
| 🎨 Frontend | React 19 + Vite + Syncfusion® EJ2 | Render the interactive Pivot Table UI |
| ⚙️ Backend | Python 3.11+ + FastAPI + Uvicorn | Serve data, perform CRUD, return JSON responses |
| 🔌 Adaptor | UrlAdaptor | Bridge between Pivot Table and FastAPI REST endpoint |
| 📊 Sample Data | In-memory PRODUCTS list (from products_data.json) | Simulate product sales records for the Pivot Table |
💡 The UrlAdaptor is ideal when you want full server-side control over query processing, filtering, and data transformation. A single
POST /products/endpoint inspects theactionproperty of the request payload (insert,update,remove, or none) and forwards it to the corresponding service handler.
- 📊 Remote Data Binding – Connects the Pivot Table to a FastAPI REST endpoint over HTTP.
- 🔄 Full CRUD Support – Insert, update, and delete records directly from the Pivot Table drill-through grid.
- 🐍 Async API Backend – Built with FastAPI for high-performance, async REST endpoints with automatic documentation (Swagger UI at
/docs). - 🗂️ Standardized Response Format – Returns data as
{ result, count }, which is whatUrlAdaptorexpects whenrequiresCountsistrue. - 🔑 Primary Key Configuration – Uses
ProductIDas the primary key for unique record identification during update and delete. - 🌐 CORS-Enabled – Preconfigured with
CORSMiddlewareto allow cross-origin requests from the Vite dev server. - ⚡ Drill-Through Editing – Double-click a pivot cell to add, edit, or delete underlying records in a pop-up grid.
- 🛡️ Robust Error Handling – Service handlers return meaningful HTTP status codes (
400,404,500) and clear error messages. - 🧩 Modular Service Layout – Insert, update, and delete logic lives in separate files under
routers/services/for easier maintenance. - 📦 Ready-to-Run – Clone, install, and start both projects — no database setup required (in-memory sample data).
Make sure the following software and packages are installed on your machine before running the project.
| Software / Package | Version | Purpose |
|---|---|---|
| 🐍 Python | 3.11 or later | Runtime for the FastAPI backend |
| 📦 venv | Included with Python | Creates an isolated Python environment for the backend |
| ⚡ FastAPI | 0.110 or later | REST API framework |
| 🚂 Uvicorn | 0.29 or later | ASGI server for running the FastAPI application |
| 🟢 Node.js | 20.x LTS or later | Runtime for the React dev server |
| 📦 npm / yarn / pnpm | Latest stable | Package manager |
| ⚛️ React | 19.x or later | Build the Pivot Table client |
| ⚡ Vite | 8.1 or later | React dev server and build tool |
| 📦 @syncfusion/ej2-react-pivotview | 33.1.45+ | React Pivot Table component |
syncfusion-react-pivot-with-fastapi-server/
├── 📁 Client/ # React frontend (Pivot Table) — Vite + TypeScript
│ ├── 📁 public/
│ ├── 📁 src/
│ │ ├── App.css # Component styles
│ │ ├── App.tsx # Pivot Table with UrlAdaptor + CRUD configuration
│ │ ├── index.css
│ │ ├── main.tsx # React entry point
│ │ └── 📁 assets/
│ ├── index.html
│ ├── package.json # React dependencies & scripts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
│
├── 📁 FastAPIServer/ # Python backend (FastAPI + Uvicorn)
│ ├── 📁 routers/
│ │ ├── __init__.py
│ │ ├── products.py # Router: loads data, defines API endpoints, routes CRUD actions
│ │ └── 📁 services/
│ │ ├── __init__.py
│ │ ├── insert.py # handle_insert() – add a new product record
│ │ ├── update.py # handle_update() – modify an existing record
│ │ └── remove.py # handle_remove() – delete a record by ProductID
│ ├── main.py # FastAPI app: CORS, router registration (/products prefix)
│ ├── products_data.json # Sample product data source (16 records)
│ └── requirements.txt # Python dependencies (fastapi, uvicorn)
│
├── 📄 README.md # You are here
└── 📄 fastapi-server.md # UG documentation source for this sample
git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-fastapi-server.git
cd syncfusion-react-pivot-with-fastapi-serverThe backend project lives in the FastAPIServer/ folder.
A virtual environment keeps the Python packages used by this backend separate from other projects on your machine.
cd FastAPIServer
python -m venv venv
# Windows (PowerShell)
.\venv\Scripts\Activate.ps1
# macOS / Linuxsource venv/bin/activatepip install -r requirements.txtThe requirements.txt file includes the following key packages:
fastapi
uvicorn[standard]
Package descriptions:
- fastapi – Creates the FastAPI application and handles REST API routing.
- uvicorn – ASGI server used to run the FastAPI application.
products_data.json provides the in-memory data source for the Pivot Table. It contains product records with the following fields.
| Field | Data type | Description |
|---|---|---|
ProductID | number | Unique product identifier (primary key) |
ProductName | string | Name of the product |
Category | string | Category to which the product belongs |
MRP | number | Maximum Retail Price of the product |
Discount | number | Discount value applied to the product |
The first three records are shown below for brevity. The complete file contains 16 product records (identical ProductName values across four Category values, with incrementing MRP and Discount).
[
{
"ProductID": 10001,
"ProductName": "Smartwatch",
"Category": "Electronics",
"MRP": 100.0,
"Discount": 1.02
},
{
"ProductID": 10002,
"ProductName": "Smartwatch",
"Category": "Accessories",
"MRP": 110.0,
"Discount": 1.12
},
{
"ProductID": 10003,
"ProductName": "Smartwatch",
"Category": "Home Appliances",
"MRP": 120.0,
"Discount": 1.22
}
]📝 The
Discountfield is included for completeness and can be used as an additional value field in the Pivot Table. The minimal report in this sample summarizes only theMRPfield, soDiscountdoes not appear indataSourceSettings.
main.py configures the FastAPI application, CORS middleware, and router registration:
# filepath: FastAPIServer/main.pyfromfastapiimportFastAPIfromfastapi.middleware.corsimportCORSMiddleware# ✅ Import from routers folderfromrouters.productsimportrouterasproducts_routerapp=FastAPI(title="Products API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ✅ Register routerapp.include_router(
products_router,
prefix="/products",
tags=["products"]
)🔒 Production CORS: Replace
allow_origins=["*"]with the actual frontend domain, for exampleallow_origins=["https://yourdomain.com"].
routers/products.py loads the product data into memory at startup and exposes the API endpoints. Field metadata (FIELDS_META) describes the field names used in the data source and is passed to the insert handler so missing fields can be defaulted to None.
A single POST /products/ endpoint inspects the action property of the request payload and routes the request to the corresponding CRUD service handler:
action value | Handler invoked |
|---|---|
insert | handle_insert() |
update | handle_update() |
remove | handle_remove() |
| (missing) | Default read response |
# filepath: FastAPIServer/routers/products.py@router.post('/', response_class=JSONResponse)asyncdeflist_or_crud(payload: Dict[str, Any]):
action=payload.get('action')
ifaction=='insert':
returnhandle_insert(payload, PRODUCTS, save_products, FIELDS_META)
ifaction=='update':
returnhandle_update(payload, PRODUCTS, save_products)
ifaction=='remove':
returnhandle_remove(payload, PRODUCTS, save_products)
# Default read operationreturnJSONResponse({'result': PRODUCTS, 'count': len(PRODUCTS)})A GET /products/ endpoint is also provided for manual verification in a browser or API testing tool.
The CRUD logic is split across separate files under routers/services/ for easier maintenance.
insert.py – handle_insert() reads the record from payload['value'] (or the payload itself), auto-generates a ProductID when it is not provided, ensures all schema fields exist on the record, appends it to the in-memory PRODUCTS list, and returns the new record.
update.py – handle_update() locates the record by ProductID (read from payload['key'], payload['ProductID'], or payload['value']['ProductID']), merges the existing record with the incoming values (preserving the key), and returns the updated record. Returns 400 if the key is missing and 404 if the record is not found.
remove.py – handle_remove() locates the record by ProductID (read from payload['key'], payload['ProductID'], or payload['record_id']), removes it from the PRODUCTS list, and returns the deleted record. Returns 400 if the key is missing and 404 if the record is not found.
⚠️ Persistence:save_products()is intentionally a no-op in the sample. Runtime CRUD changes are kept only in memory and are discarded when the server restarts (the original contents ofproducts_data.jsonare reloaded on every server start via_load_products()). To persist changes, replacesave_products()with logic that writes back toproducts_data.jsonor a database.
The React client lives in the Client/ folder.
cd ../Client
npm installnpm install @syncfusion/ej2-react-pivotview @syncfusion/ej2-dataOpen src/App.tsx and ensure the url in the DataManager points to your backend port (default in this repo: 8000).
// filepath: Client/src/App.tsximport*asReactfrom'react';import{PivotViewComponent,CellEditSettings,Inject,FieldList}from'@syncfusion/ej2-react-pivotview';import{DataManager,UrlAdaptor}from'@syncfusion/ej2-data';importtype{DataSourceSettingsModel}from'@syncfusion/ej2-pivotview/src/model/datasourcesettings-model';importtype{BeginDrillThroughEventArgs}from'@syncfusion/ej2-pivotview';import'./App.css';functionApp(): React.ReactElement{// Configure DataManager with UrlAdaptor.constdata: DataManager=newDataManager({url: 'http://localhost:8000/products/',adaptor: newUrlAdaptor(),crossDomain: true,});constdataSourceSettings: DataSourceSettingsModel={dataSource: data,expandAll: true,rows: [{name: 'ProductName'}],columns: [{name: 'Category'}],values: [{name: 'MRP'}],filters: [],};// Enable editing functionalityconsteditSettings: CellEditSettings={allowEditing: true,// Enables the Edit button and allows users to modify existing records.allowAdding: true,// Enables the Add button and allows users to create new records.allowDeleting: true,// Enables the Delete button and allows users to remove records.mode: 'Normal'// Uses Normal mode (inline editing); other options: 'Dialog', 'Batch', 'CommandColumn'.};constpivotObj=React.useRef<PivotViewComponent>(null);// Configure beginDrillThrough event to set the primary key for CRUD operationsfunctionbeginDrillThrough(args: BeginDrillThroughEventArgs){// Iterate through all columns in the drill-through gridfor(leti=0;i<args.gridObj.columns.length;i++){// Check if the current column is the primary key columnif(args.gridObj.columns[i].field==="ProductID"){args.gridObj.columns[i].visible=true;// Mark this column as the primary key// This tells DataManager to use this column's value to uniquely identify recordsargs.gridObj.columns[i].isPrimaryKey=true;}}}return(<divclassName='control-section'style={{margin: 100}}><PivotViewComponentref={pivotObj}id='PivotView'height={350}width={700}dataSourceSettings={dataSourceSettings}showFieldList={true}editSettings={editSettings}beginDrillThrough={beginDrillThrough}><Injectservices={[FieldList]}/></PivotViewComponent></div>);}exportdefaultApp;📝 If your FastAPI server runs on a different port, update the
urlvalue inClient/src/App.tsxaccordingly. The default in this repo is8000.
Code explanation:
- DataManager – Configured with the FastAPI endpoint at
http://localhost:8000/products/to retrieve product data. - UrlAdaptor – Sends
POSTrequests to the configured endpoint and processes the JSON response returned by the FastAPI backend. - dataSourceSettings – Defines the Pivot Table report layout.
rows– Displays ProductName values as row headers.columns– Displays Category values as column headers.values– Summarizes the MRP field for each row and column combination.
- editSettings – Enables add, edit, and delete operations on the drill-through grid.
- beginDrillThrough – Marks the
ProductIDcolumn as the primary key (isPrimaryKey = true) before the drill-through grid is displayed, so update and delete operations target the correct record. - FieldList – Displays the Field List and allows fields to be rearranged across rows, columns, values, and filters.
You need two terminals — one for the backend API and one for the React client.
Make sure your virtual environment is activated (see step 2.1), then from the FastAPIServer folder run:
uvicorn main:app --reload --port 8000The server will start and listen on http://localhost:8000 by default.
Verify it works:
- 🌐 Open
http://localhost:8000/products/in your browser, or use a tool like Postman/curl. - 📖 Interactive API docs are available at
http://localhost:8000/docs(Swagger UI provided by FastAPI). - ✅ You should see a JSON response containing the product records as
{ result, count }.
Sample request via curl:
curl -X POST http://localhost:8000/products/ \
-H "Content-Type: application/json" \
-d '{"requiresCounts": true, "skip": 0, "take": 10}'Sample response:
{
"result": [
{ "ProductID": 10001, "ProductName": "Smartwatch", "Category": "Electronics", "MRP": 100.0, "Discount": 1.02 },
{ "ProductID": 10002, "ProductName": "Smartwatch", "Category": "Accessories", "MRP": 110.0, "Discount": 1.12 }
],
"count": 16
}📝 Note the port number in the terminal output and update the
urlinClient/src/App.tsxif it is different from8000.
cd Client
npm run devThe Vite dev server will start and display a URL (typically http://localhost:5173).
- Open the URL printed by Vite in your browser.
- You should see the Pivot Table populated with aggregated MRP values, grouped by ProductName (rows) and Category (columns).
- Open the browser's Developer Tools (F12) → Network tab.
- Reload the page.
- You should see a
POSTrequest tohttp://localhost:8000/products/with status200and a JSON response containing the product records. - The Pivot Table renders the aggregated data automatically.
The Pivot Table supports full CRUD through its built-in drill-through editing grid.
| Step | Action | Expected Action on Backend |
|---|---|---|
| 1️⃣ | Double-click any pivot cell to open the drill-through grid showing underlying source records. | Initial POST /products/ (read) |
| ➕ 2️⃣ | Click Add, fill in the new row fields, then click Update. | POST /products/ with action: "insert" |
| ✏️ 3️⃣ | Click Edit on an existing row, change a field, then click Update. | POST /products/ with action: "update" |
| 🗑️ 4️⃣ | Click Delete on a row to remove it. | POST /products/ with action: "remove" |
| 🔁 5️⃣ | The Pivot Table automatically refreshes to display the updated aggregated data from the backend. | New POST /products/ (read) |
🔑 The
ProductIDcolumn is automatically marked as the primary key inside thebeginDrillThroughevent, so update and delete operations know which record to target.
⚠️ Becausesave_products()is a no-op by design, any CRUD changes made at runtime are kept only in memory and are discarded when the server is restarted. This is expected behavior for the sample.
| ❓ Issue | 🔍 Symptom | ✅ Resolution |
|---|---|---|
| 🚫 Empty Pivot Table | Pivot loads with no errors but no rows or values appear. | Verify that the FastAPI endpoint returns data and that the response contains both the result and count properties. Ensure the field names returned by the backend match the fields configured in dataSourceSettings (case-sensitive). |
| 🐍 500 Internal Server Error | The Pivot Table fails and the browser shows a server error. | Check the server console for error messages. Verify that products_data.json exists, contains valid JSON, and can be read by the backend. |
| 💥 500 on insert with empty data | FastAPI returns a 500 error when adding a record. | handle_insert() computes the new ProductID with max([...]) + 1, which fails if the product list is empty. Ensure products_data.json is not empty. |
| 404 Not Found | Updating or deleting a record returns a 404 error. | Verify that the ProductID sent in the request matches an existing record and that the record has not already been deleted. |
| 🔄 CRUD operation ignored / falls back to read | A record is added, updated, or deleted, but the backend always returns the full product list. | Verify that the request payload includes the correct action value (insert, update, or remove). When action is missing or unrecognized, the router returns the default read response. |
| 💾 CRUD operations not saving | The edit dialog closes but changes are not reflected in the data. | Verify editing is enabled through editSettings and that ProductID is configured as the primary key in the beginDrillThrough event. |
| 🧹 Changes lost after server restart | Records added, updated, or deleted earlier disappear when the FastAPI server is restarted. | This is expected with the sample backend; save_products() is a no-op by design. To persist changes, implement file/database writes inside save_products(). |
| 🔄 Changes not reflected in Pivot Table | A CRUD operation completes successfully, but the Pivot Table still shows the old data. | Verify the backend processed the request successfully and returned updated data. Check the browser's Network tab for failed requests. If needed, call pivotObj.current?.refresh(); after an operation. |
| 🌐 CORS Blocked | Console shows Access to XMLHttpRequest ... has been blocked by CORS policy. | Verify CORSMiddleware is registered in main.py and that allow_origins permits your dev server's origin. |
| 🔤 Property casing mismatch | Pivot appears empty or shows "field not found" even though the API returns data. | Ensure field names in the API response match the Pivot Table's dataSourceSettings (e.g., ProductID, ProductName). |
| 🔌 Wrong port | The frontend cannot reach the backend. | Confirm the url in Client/src/App.tsx matches the port the FastAPI server is listening on (default 8000). |
| 📦 Missing Python packages | The server fails to start with ModuleNotFoundError. | Ensure your virtual environment is activated and pip install -r requirements.txt has been run. |
| 🔁 Invalid JSON response | Data cannot be loaded even though the request succeeds. | Verify the backend returns a valid JSON response whose structure matches the expected { result, count } format. |
If issues persist, use the browser's Developer Tools (F12) to inspect the Network and Console tabs.
The backend exposes endpoints through the products router. The Syncfusion DataManager with UrlAdaptor issues POST requests to the single /products/ endpoint; the action property in the request payload determines which operation is performed.
| Method | Route | Action payload | Purpose | Response |
|---|---|---|---|---|
GET | /products/ | (none) | Retrieve product records (manual verification) | { result: [...], count: n } |
POST | /products/ | (no action) | Retrieve product records (read from Pivot Table) | { result: [...], count: n } |
POST | /products/ | { "action": "insert", "value": { ... } } | Insert a new product | The newly added product record |
POST | /products/ | { "action": "update", "key": ProductID, "value": { ... } } | Update an existing product (matched by ProductID) | The updated product record |
POST | /products/ | { "action": "remove", "key": ProductID } | Delete a product by primary key | The deleted product record |
📖 Interactive Swagger UI documentation is available at
http://localhost:8000/docsonce the server is running.
The ProductDetails model exposes the following fields:
| Field | Type | Description |
|---|---|---|
ProductID | number | Unique product identifier (primary key) |
ProductName | string | Name of the product |
Category | string | Category to which the product belongs |
MRP | number | Maximum Retail Price of the product |
Discount | number | Discount value applied to the product |
Contributions are welcome and appreciated! 💖
- 🍴 Fork the repository.
- 🌿 Create a feature branch:
git checkout -b feature/my-awesome-change - 💾 Commit your changes:
git commit -m "Add my awesome change" - 📤 Push to your branch:
git push origin feature/my-awesome-change - 🔁 Open a Pull Request describing the change and its motivation.
- Follow the existing code style in both the React and FastAPI projects.
- Keep changes focused — one feature or fix per pull request.
- Update or add documentation (
README.md,fastapi-server.md) when behavior changes. - Test your changes locally against both the backend and frontend before submitting.
This project is released under the MIT License. You are free to use, modify, and distribute the code in personal and commercial projects. See the LICENSE file for full text.
- 📘 Documentation:Syncfusion® React Pivot Table Docs
- 💬 Community forum:Syncfusion® Community
- 🐛 Bug reports & feature requests:GitHub Issues
- 📧 Direct support:Syncfusion® Support Portal (for licensed users)
- 📖 UrlAdaptor Guide:UrlAdaptor Documentation
- ⚡ FastAPI Reference:FastAPI Documentation
- 🚂 Uvicorn Reference:Uvicorn Documentation
⭐ If this project helped you, please consider giving it a star on GitHub — it helps others discover it!
- 🔗 Syncfusion® React Pivot Table – Getting Started
- 📘 PivotTable Data Binding
- 📘 DataManager Getting Started
- 📘 UrlAdaptor Reference
- 📘 UrlAdaptor with Pivot Table
- 📘 PivotTable Editing
- 📘 PivotTable Drill-Through
- ⚡ FastAPI Documentation
- 🐍 Python venv Guide
- 🚂 Uvicorn Settings