A production-ready quick start that connects the Syncfusionยฎ React Pivot Table to an Express.js (Node.js + TypeScript) 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 Express.js backend using the UrlAdaptor of the DataManager. The UrlAdaptor provides full control over the request and response format, making it the perfect fit for any REST API โ including lightweight Node.js servers built with Express.
| Component | Technology | Purpose |
|---|---|---|
| ๐จ Frontend | React 19 + Vite + Syncfusionยฎ EJ2 | Render the interactive Pivot Table UI |
| โ๏ธ Backend | Express.js 4.18 + TypeScript 5.3 | Serve data, perform CRUD, return JSON responses |
| ๐ Adaptor | UrlAdaptor |
Bridge between Pivot Table and Express REST API |
| ๐ Sample Data | In-memory productDetails list |
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. Because every request is a
POST, the same endpoint and code path can handle read, insert, update, and delete operations.
- ๐ Remote Data Binding โ Connects the Pivot Table to an Express.js REST endpoint over HTTP.
- ๐ Full CRUD Support โ Insert, update, and delete records directly from the Pivot Table drill-through grid.
- ๐จ Type-Safe Backend โ Built with TypeScript for compile-time safety on controllers, routes, and data models.
- ๐๏ธ Standardized Response Format โ Returns data as
{ result, count }whenrequiresCountsistrue, or as a plain array otherwise. - ๐ Primary Key Configuration โ Uses
ProductIDas the primary key for unique record identification. - ๐ CORS-Enabled โ Preconfigured with
corsmiddleware to 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 โ Controllers wrap logic in
try/catchand return meaningful HTTP status codes (422, 404, 500). - ๐ฆ 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 |
|---|---|---|
| ๐ข Node.js | 20.x LTS or later | Runtime for the Express.js server and React dev server |
| ๐ฆ npm / yarn / pnpm | Latest stable | Package manager |
| โ๏ธ React | 19.x or later | Build the Pivot Table client |
| ๐ฃ TypeScript | 5.3 or later | Type-safe backend development |
| ๐ Express | ^4.18.2 | Framework for building the REST API |
| ๐ cors | ^2.8.5 | Enables CORS between the React client and Express |
| โก Vite | 8.1 or later | React dev server and build tool |
| ๐ฆ @syncfusion/ej2-react-pivotview | 33.1.45+ | React Pivot Table component |
| ๐ฆ @syncfusion/ej2-data | 33.1.45+ | DataManager and UrlAdaptor |
syncfusion-react-pivot-with-express-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
โ
โโโ ๐ ExpressServer/ # Express.js backend (Node.js + TypeScript)
โ โโโ ๐ src/
โ โ โโโ ๐ controllers/
โ โ โ โโโ products.controller.ts # CRUD handlers: get, create, update, delete
โ โ โโโ ๐ routes/
โ โ โ โโโ products.routes.ts # POST routes for /, /create, /update, /remove
โ โ โโโ ๐ types/
โ โ โ โโโ interface.ts # ProductDetails & DataManagerRequest interfaces
โ โ โโโ ๐ utils/
โ โ โโโ data.ts # In-memory sample product data
โ โโโ package.json # Express dependencies & scripts
โ โโโ server.ts # Express app: CORS, JSON parsing, route mounting
โ โโโ tsconfig.json
โ
โโโ ๐ README.md # You are here
README reference
git clone https://github.com/SyncfusionExamples/syncfusion-react-pivot-with-express-server.git
cd syncfusion-react-pivot-with-express-serverThe backend project lives in the ExpressServer/ folder.
cd ExpressServer
npm installThe package.json should include the following key packages:
{
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/cors": "^2.8.17",
"@types/node": "^20.10.6",
"typescript": "^5.3.3",
"ts-node": "^10.9.2"
}
}Package descriptions:
- express โ Creates the Express.js server and handles REST API routes.
- cors โ Allows cross-origin requests from the React client to the Express server.
- typescript โ Adds TypeScript support to the backend.
- ts-node โ Runs TypeScript files directly without a separate build step.
- @types/express, @types/cors, @types/node โ Provide TypeScript type definitions.
Available scripts:
npm run devโ Starts the Express server in development mode usingts-node.npm run buildโ Compiles TypeScript files into thedist/folder.npm startโ Runs the compiled server (node dist/server.js).
server.ts already configures:
- โ
CORS with
origin: '*'for development (restrict this in production). - โ JSON body parsing for request payloads from the DataManager.
- โ URL-encoded body parsing as a fallback.
- โ
Route mounting at
/api/products.
// filepath: ExpressServer/server.ts
import express, { Application } from 'express';
import cors from 'cors';
import productsRoutes from './src/routes/products.routes';
const app: Application = express();
const PORT = process.env.PORT ? Number(process.env.PORT) : 5000;
// Enable CORS for all origins (configure as needed for production)
// The methods list reflects that the Syncfusion DataManager uses POST
// for all read and CRUD operations when paired with UrlAdaptor.
app.use(cors({
origin: '*',
methods: ['POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Parse JSON request bodies
app.use(express.json());
// Parse URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
// Mount product routes
app.use('/api/products', productsRoutes);
app.listen(PORT, () => {
console.log(`Products endpoint: http://localhost:${PORT}/api/products`);
});
export default app;๐ Production CORS: Replace
origin: '*'with the actual frontend domain, for exampleorigin: 'https://yourdomain.com'.
src/types/interface.ts defines the TypeScript contracts used throughout the backend:
// filepath: ExpressServer/src/types/interface.ts
export interface ProductDetails {
ProductID?: number;
ProductName?: string;
Category?: string;
MRP?: number;
Discount?: number;
}
export interface DataManagerRequest {
skip?: number;
take?: number;
requiresCounts?: boolean;
}src/utils/data.ts exports an in-memory productDetails array used by the controllers.
src/controllers/products.controller.ts exposes four handlers: getProducts, createProduct, updateProduct, and deleteProduct. Each handler:
- Wraps the logic in a
try/catchblock. - Returns appropriate HTTP status codes (
200,201,404,422,500). - Reads the payload from
req.body.value || req.bodyso it works with both the UrlAdaptor's wrapped payload and direct POSTs.
src/routes/products.routes.ts mounts the controllers as POST endpoints:
// filepath: ExpressServer/src/routes/products.routes.ts
import { Router } from 'express';
import { getProducts, createProduct, updateProduct, deleteProduct } from '../controllers/products.controller';
const router = Router();
router.post('/', (req, res) => {
return getProducts(req, res);
});
router.post('/create', (req, res) => { return createProduct(req, res); });
router.post('/update', (req, res) => { return updateProduct(req, res); });
router.post('/remove', (req, res) => { return deleteProduct(req, res); });
export default router;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: 5000).
// filepath: Client/src/App.tsx
import * as React from 'react';
import { PivotViewComponent, CellEditSettings, Inject, FieldList } from '@syncfusion/ej2-react-pivotview';
import { DataManager, UrlAdaptor } from '@syncfusion/ej2-data';
import type { DataSourceSettingsModel } from '@syncfusion/ej2-pivotview/src/model/datasourcesettings-model';
import type { BeginDrillThroughEventArgs } from '@syncfusion/ej2-pivotview';
import './App.css';
function App(): React.ReactElement {
// Configure DataManager with UrlAdaptor.
const API_BASE_URL = 'http://localhost:5000/api/products';
const data: DataManager = new DataManager({
url: API_BASE_URL,
insertUrl: API_BASE_URL + '/create',
updateUrl: API_BASE_URL + '/update',
removeUrl: API_BASE_URL + '/remove',
adaptor: new UrlAdaptor()
});
const dataSourceSettings: DataSourceSettingsModel = {
dataSource: data,
expandAll: true,
rows: [{ name: 'ProductID' }],
columns: [{ name: 'Category' }],
values: [{ name: 'MRP' }],
filters: [],
};
// Enable editing functionality
const editSettings: 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 (popup dialog) for editing; other options: 'Dialog', 'Batch', 'CommandColumn'.
};
const pivotObj = React.useRef<PivotViewComponent>(null);
// Configure beginDrillThrough event to set the primary key for CRUD operations
function beginDrillThrough(args: BeginDrillThroughEventArgs) {
// Iterate through all columns in the drill-through grid
for (var i = 0; i < args.gridObj.columns.length; i++) {
// Check if the current column is the primary key column
if (args.gridObj.columns[i].field === "ProductID") {
// Mark this column as the primary key
// This tells DataManager to use this column's value to uniquely identify records
args.gridObj.columns[i].isPrimaryKey = true;
}
}
}
return (
<div className='control-section' style={{ margin: 100 }}>
<PivotViewComponent
ref={pivotObj}
id='PivotView'
height={350}
width={700}
dataSourceSettings={dataSourceSettings}
showFieldList={true}
editSettings={editSettings}
beginDrillThrough={beginDrillThrough}
>
<Inject services={[FieldList]} />
</PivotViewComponent>
</div>
);
}
export default App;๐ If your Express server runs on a different port, update
API_BASE_URLaccordingly. The same value is also used forinsertUrl,updateUrl, andremoveUrl.
You need two terminals โ one for the backend API and one for the React client.
cd ExpressServer
npm run devThe server will start and listen on http://localhost:5000 by default.
Verify it works:
- ๐ Open
http://localhost:5000/api/productsin your browser or use a tool like Postman/curl. - โ
You should see a JSON response containing the product records (array, or
{ result, count }ifrequiresCountsistrue).
Sample request via curl:
curl -X POST http://localhost:5000/api/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": "USB Book Light", "Category": "Accessories", "MRP": 100.0, "Discount": 0.20 }
],
"count": 25
}๐ Note the port number in the terminal output and update
API_BASE_URLinClient/src/App.tsxif it is different from5000.
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 ProductID (rows) and Category (columns).
- Open the browser's Developer Tools (F12) โ Network tab.
- Reload the page.
- You should see a
POSTrequest tohttp://localhost:5000/api/productswith 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 Network Request |
|---|---|---|
| 1๏ธโฃ | Double-click any pivot cell to open the drill-through grid showing underlying source records. | Initial POST /api/products |
| โ 2๏ธโฃ | Click Add, fill in the new row fields, then click Update. | POST http://localhost:5000/api/products/create |
| โ๏ธ 3๏ธโฃ | Click Edit on an existing row, change a field, then click Update. | POST http://localhost:5000/api/products/update |
| ๐๏ธ 4๏ธโฃ | Click Delete on a row to remove it. | POST http://localhost:5000/api/products/remove |
| ๐ 5๏ธโฃ | The Pivot Table automatically refreshes to display the updated aggregated data from the backend. | New POST /api/products |
๐ The
ProductIDcolumn is automatically marked as the primary key inside thebeginDrillThroughevent, so update and delete operations know which record to target.
| โ Issue | ๐ Symptom | โ Resolution |
|---|---|---|
| ๐ซ Empty Pivot Table | Pivot loads with no errors but no rows or values appear. | Ensure the API returns an array (or { result, count }) and that field names match the dataSourceSettings (case-sensitive). |
| 404 Not Found | Network tab shows a 404 response when the Pivot Table loads. | Confirm the backend is running, the route prefix in server.ts is /api/products, and the url in App.tsx matches. |
| ๐ฅ 500 Internal Server Error | The Pivot Table fails and the browser shows a server error. | Check the terminal output for stack traces. Common causes: a missing productDetails import or a malformed request body. |
| ๐ CORS Blocked | Console shows Access to XMLHttpRequest ... has been blocked by CORS policy. |
Verify the cors middleware is registered in server.ts and that origin is set to a value that allows your dev server. |
| ๐พ CRUD operations not saving | The edit dialog closes but changes are not reflected in the data. | Confirm the primary key is set in beginDrillThrough and that the CRUD routes (/create, /update, /remove) are mounted. |
| ๐ค 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). |
| ๐ข Pivot Table loads slowly | Rendering is sluggish or the browser hangs. | Make sure requiresCounts is honored so the count field is returned and paging/virtual scrolling can be enabled. |
| ๐ Wrong port | The frontend cannot reach the backend. | Confirm API_BASE_URL in Client/src/App.tsx matches the port the Express server is listening on (default 5000). |
| ๐ ๏ธ TypeScript compile errors | npm run dev fails with TypeScript errors. |
Run npm install to ensure @types/express, @types/cors, and @types/node are installed, then retry. |
| ๐ Changes Not Reflected in Pivot Table | A CRUD operation completes successfully, but the Pivot Table still shows the old data. | Programmatically refresh the Pivot Table by calling pivotObj.current?.refresh(); after each operation if needed. |
The backend exposes the following endpoints through productsRoutes. All endpoints accept POST requests because the Syncfusion DataManager with UrlAdaptor issues POST for read and CRUD operations.
| Method | Route | Purpose | Request Body | Response |
|---|---|---|---|---|
POST |
/api/products |
Retrieve product records (read) | { skip?, take?, requiresCounts? } |
Array of products, or { result, count } |
POST |
/api/products/create |
Insert a new product | ProductDetails JSON (or { value }) |
201 Created with the new product |
POST |
/api/products/update |
Update an existing product (matched by ProductID) |
ProductDetails JSON (or { value }) |
200 OK with the updated product |
POST |
/api/products/remove |
Delete a product by primary key | { key: ProductID } |
200 OK with confirmation message |
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 Express.js projects.
- Keep changes focused โ one feature or fix per pull request.
- Update or add documentation (
README.md,express-js-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
- ๐ Express.js Reference: Express.js Documentation
โญ If this project helped you, please consider giving it a star on GitHub โ it helps others discover it!
- ๐ WebApiAdaptor with Pivot Table โ Companion sample using
WebApiAdaptorwith an ASP.NET Core Web API. - ๐ PivotTable Data Binding
- ๐ DataManager Getting Started
- ๐ UrlAdaptor Reference
- ๐ PivotTable Editing
- ๐ PivotTable Drill-Through
- ๐ Express.js Routing Guide
- ๐ Express CORS Middleware