Build a Data Grid with Server-side Data
Overview
This tutorial walks you through building a Data Grid that fetches data from a server with pagination, sorting, and filtering. The primary purpose is to demonstrate the use of the Data Source layer, which streamlines the development of key Data Grid features when working with server-side data by providing an interface for communications between the Grid on the client and the data on the server.
Prerequisites
- Basic React knowledge
- Understanding of TypeScript interfaces
- Familiarity with async/await and fetch APIs
Relevant documentation
The docs listed below may be useful if you're new to the MUI X Data Grid:
Part one: App setup
In part one, you'll set up the basic scaffolding for a full-stack React app using Vite on the front end and Express.js for the back end.
1. Create the project structure
Create a new directory and set up the folder structure:
mkdir server-side-data &&
cd server-side-data &&
mkdir client server
2. Initialize the server
Navigate to the server directory and initialize:
cd server &&
pnpm init
Install server dependencies:
pnpm install express cors &&
pnpm install --save-dev typescript @types/express @types/node tsx
Create a tsconfig.json file:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
3. Initialize the client
Navigate to the client directory and create a React app with Vite in TypeScript:
cd ../client &&
pnpm create vite@latest . -- --template react-ts &&
pnpm install
Install Material UI and MUI X dependencies:
pnpm install @mui/material @emotion/react @emotion/styled @mui/icons-material @mui/x-data-grid @fontsource/roboto
4. Set up the server code
Create server/src/index.ts and add the following code:
import express from 'express';
import cors from 'cors';
const app = express();
const PORT = 3001;
// Middleware
app.use(cors());
app.use(express.json());
// Dummy data - simulating a database
const dummyData = [
{
id: 1,
name: 'John Doe',
email: 'john@example.com',
role: 'Developer',
department: 'Engineering',
salary: 75000,
startDate: '2023-01-15',
},
{
id: 2,
name: 'Jane Smith',
email: 'jane@example.com',
role: 'Designer',
department: 'Design',
salary: 65000,
startDate: '2023-02-20',
},
{
id: 3,
name: 'Bob Johnson',
email: 'bob@example.com',
role: 'Manager',
department: 'Product',
salary: 85000,
startDate: '2022-11-10',
},
{
id: 4,
name: 'Alice Brown',
email: 'alice@example.com',
role: 'Developer',
department: 'Engineering',
salary: 80000,
startDate: '2023-03-05',
},
{
id: 5,
name: 'Charlie Wilson',
email: 'charlie@example.com',
role: 'Analyst',
department: 'Data',
salary: 70000,
startDate: '2023-04-12',
},
{
id: 6,
name: 'Diana Davis',
email: 'diana@example.com',
role: 'Designer',
department: 'Design',
salary: 68000,
startDate: '2023-01-30',
},
{
id: 7,
name: 'Eve Miller',
email: 'eve@example.com',
role: 'Developer',
department: 'Engineering',
salary: 82000,
startDate: '2022-12-18',
},
{
id: 8,
name: 'Frank Garcia',
email: 'frank@example.com',
role: 'Manager',
department: 'Sales',
salary: 90000,
startDate: '2022-09-25',
},
{
id: 9,
name: 'Grace Lee',
email: 'grace@example.com',
role: 'Analyst',
department: 'Data',
salary: 72000,
startDate: '2023-05-08',
},
{
id: 10,
name: 'Henry Taylor',
email: 'henry@example.com',
role: 'Developer',
department: 'Engineering',
salary: 78000,
startDate: '2023-06-14',
},
{
id: 11,
name: 'Ivy Chen',
email: 'ivy@example.com',
role: 'Developer',
department: 'Engineering',
salary: 76000,
startDate: '2023-07-22',
},
{
id: 12,
name: 'Jack Anderson',
email: 'jack@example.com',
role: 'Designer',
department: 'Design',
salary: 67000,
startDate: '2023-08-15',
},
{
id: 13,
name: 'Kate Martinez',
email: 'kate@example.com',
role: 'Manager',
department: 'Marketing',
salary: 88000,
startDate: '2023-09-03',
},
{
id: 14,
name: 'Liam Thompson',
email: 'liam@example.com',
role: 'Analyst',
department: 'Data',
salary: 71000,
startDate: '2023-10-11',
},
{
id: 15,
name: 'Maya Rodriguez',
email: 'maya@example.com',
role: 'Developer',
department: 'Engineering',
salary: 79000,
startDate: '2023-11-28',
},
{
id: 16,
name: 'Noah White',
email: 'noah@example.com',
role: 'Designer',
department: 'Design',
salary: 66000,
startDate: '2023-12-05',
},
{
id: 17,
name: 'Olivia Harris',
email: 'olivia@example.com',
role: 'Manager',
department: 'HR',
salary: 87000,
startDate: '2024-01-14',
},
{
id: 18,
name: 'Paul Clark',
email: 'paul@example.com',
role: 'Analyst',
department: 'Data',
salary: 73000,
startDate: '2024-02-20',
},
{
id: 19,
name: 'Quinn Lewis',
email: 'quinn@example.com',
role: 'Developer',
department: 'Engineering',
salary: 81000,
startDate: '2024-03-08',
},
{
id: 20,
name: 'Ruby Hall',
email: 'ruby@example.com',
role: 'Designer',
department: 'Design',
salary: 69000,
startDate: '2024-04-12',
},
{
id: 21,
name: 'Sam Young',
email: 'sam@example.com',
role: 'Manager',
department: 'Finance',
salary: 92000,
startDate: '2024-05-18',
},
{
id: 22,
name: 'Tara King',
email: 'tara@example.com',
role: 'Analyst',
department: 'Data',
salary: 74000,
startDate: '2024-06-25',
},
{
id: 23,
name: 'Uma Patel',
email: 'uma@example.com',
role: 'Developer',
department: 'Engineering',
salary: 77000,
startDate: '2024-07-30',
},
{
id: 24,
name: 'Victor Moore',
email: 'victor@example.com',
role: 'Designer',
department: 'Design',
salary: 64000,
startDate: '2024-08-14',
},
{
id: 25,
name: 'Wendy Scott',
email: 'wendy@example.com',
role: 'Manager',
department: 'Operations',
salary: 86000,
startDate: '2024-09-22',
},
{
id: 26,
name: 'Xander Green',
email: 'xander@example.com',
role: 'Analyst',
department: 'Data',
salary: 75000,
startDate: '2024-10-05',
},
{
id: 27,
name: 'Yara Adams',
email: 'yara@example.com',
role: 'Developer',
department: 'Engineering',
salary: 80000,
startDate: '2024-11-12',
},
{
id: 28,
name: 'Zane Baker',
email: 'zane@example.com',
role: 'Designer',
department: 'Design',
salary: 67000,
startDate: '2024-12-18',
},
{
id: 29,
name: 'Aria Nelson',
email: 'aria@example.com',
role: 'Manager',
department: 'Legal',
salary: 95000,
startDate: '2025-01-25',
},
{
id: 30,
name: 'Blake Carter',
email: 'blake@example.com',
role: 'Analyst',
department: 'Data',
salary: 72000,
startDate: '2025-02-28',
},
{
id: 31,
name: 'Cora Mitchell',
email: 'cora@example.com',
role: 'Developer',
department: 'Engineering',
salary: 78000,
startDate: '2025-03-15',
},
{
id: 32,
name: 'Dexter Perez',
email: 'dexter@example.com',
role: 'Designer',
department: 'Design',
salary: 65000,
startDate: '2025-04-20',
},
{
id: 33,
name: 'Echo Roberts',
email: 'echo@example.com',
role: 'Manager',
department: 'Sales',
salary: 89000,
startDate: '2025-05-10',
},
{
id: 34,
name: 'Finn Turner',
email: 'finn@example.com',
role: 'Analyst',
department: 'Data',
salary: 71000,
startDate: '2025-06-18',
},
{
id: 35,
name: 'Gemma Phillips',
email: 'gemma@example.com',
role: 'Developer',
department: 'Engineering',
salary: 82000,
startDate: '2025-07-25',
},
{
id: 36,
name: 'Hawk Campbell',
email: 'hawk@example.com',
role: 'Designer',
department: 'Design',
salary: 68000,
startDate: '2025-08-30',
},
{
id: 37,
name: 'Indigo Parker',
email: 'indigo@example.com',
role: 'Manager',
department: 'Product',
salary: 87000,
startDate: '2025-09-05',
},
{
id: 38,
name: 'Jasper Evans',
email: 'jasper@example.com',
role: 'Analyst',
department: 'Data',
salary: 73000,
startDate: '2025-10-12',
},
{
id: 39,
name: 'Kai Edwards',
email: 'kai@example.com',
role: 'Developer',
department: 'Engineering',
salary: 79000,
startDate: '2025-11-20',
},
{
id: 40,
name: 'Luna Collins',
email: 'luna@example.com',
role: 'Designer',
department: 'Design',
salary: 66000,
startDate: '2025-12-28',
},
];
// API Routes
app.get('/api/employees', (req, res) => {
const { page = 0, pageSize = 40, sortModel = [], filterModel = {} } = req.query;
let filteredData = [...dummyData];
// Apply filtering
if (filterModel && typeof filterModel === 'string') {
try {
const filters = JSON.parse(filterModel as string);
if (filters.items && filters.items.length > 0) {
filteredData = filteredData.filter((item) => {
const logicOperator = filters.logicOperator || 'Or';
if (logicOperator === 'And') {
// All filter items must match
return filters.items.every((filterItem: any) => {
const value = item[filterItem.field as keyof typeof item];
if (filterItem.operator === 'contains') {
return String(value)
.toLowerCase()
.includes(filterItem.value.toLowerCase());
}
if (filterItem.operator === 'equals') {
return String(value) === filterItem.value;
}
if (filterItem.operator === 'startsWith') {
return String(value)
.toLowerCase()
.startsWith(filterItem.value.toLowerCase());
}
if (filterItem.operator === 'endsWith') {
return String(value)
.toLowerCase()
.endsWith(filterItem.value.toLowerCase());
}
if (filterItem.operator === 'isEmpty') {
return !value || String(value).trim() === '';
}
if (filterItem.operator === 'isNotEmpty') {
return value && String(value).trim() !== '';
}
return true;
});
}
// At least one filter item must match (OR logic)
return filters.items.some((filterItem: any) => {
const value = item[filterItem.field as keyof typeof item];
if (filterItem.operator === 'contains') {
return String(value)
.toLowerCase()
.includes(filterItem.value.toLowerCase());
}
if (filterItem.operator === 'equals') {
return String(value) === filterItem.value;
}
if (filterItem.operator === 'startsWith') {
return String(value)
.toLowerCase()
.startsWith(filterItem.value.toLowerCase());
}
if (filterItem.operator === 'endsWith') {
return String(value)
.toLowerCase()
.endsWith(filterItem.value.toLowerCase());
}
if (filterItem.operator === 'isEmpty') {
return !value || String(value).trim() === '';
}
if (filterItem.operator === 'isNotEmpty') {
return value && String(value).trim() !== '';
}
return true;
});
});
}
} catch (error) {
// Invalid filter, return all data
}
}
// Apply sorting
if (sortModel && typeof sortModel === 'string') {
try {
const sorts = JSON.parse(sortModel as string);
if (sorts.length > 0) {
filteredData.sort((a, b) => {
for (const sort of sorts) {
const aVal = a[sort.field as keyof typeof a];
const bVal = b[sort.field as keyof typeof b];
if (aVal < bVal) {
return sort.sort === 'desc' ? 1 : -1;
}
if (aVal > bVal) {
return sort.sort === 'desc' ? -1 : 1;
}
}
return 0;
});
}
} catch (error) {
// Invalid sort, keep original order
}
}
// Apply pagination
const startIndex = Number(page) * Number(pageSize);
const endIndex = startIndex + Number(pageSize);
const paginatedData = filteredData.slice(startIndex, endIndex);
return res.json({
data: paginatedData,
total: filteredData.length,
page: Number(page),
pageSize: Number(pageSize),
});
});
app.get('/api/employees/:id', (req, res) => {
const id = Number(req.params.id);
const employee = dummyData.find((emp) => emp.id === id);
if (!employee) {
return res.status(404).json({ error: 'Employee not found' });
}
return res.json(employee);
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
// Server started
});
What's happening here:
- Express.js server is set up with CORS enabled for cross-origin requests
- Dummy employee data simulates a database
/api/employeesendpoint is created with pagination, sorting, and filtering- Filters can be applied based on column field, operator, and value
- Data can be sorted by specified fields in ascending or descending order
- Results are paginated using
pageandpageSizeparameters - Data is returned in the format expected by the Data Grid
5. Add scripts to package.json files
In server/package.json, add:
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
6. Set up the client code
In the client directory, add a new directory called components.
Inside components, add a new file named EmployeeDataGrid.tsx with the following boilerplate.
(You'll build out this component in part two of this tutorial.)
import { useMemo } from 'react';
import {
DataGrid,
type GridColDef,
type GridDataSource,
type GridGetRowsParams,
type GridGetRowsResponse,
} from '@mui/x-data-grid';
import { Box, Typography } from '@mui/material';
const EmployeeDataGrid = () => {
return (
<Box sx={{ height: 600, width: '100%' }}>
<Typography variant="h4" component="h1" gutterBottom>
MUI X Data Grid with the Data Source layer
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Server-side data with pagination, sorting, and filtering.
</Typography>
</Box>
);
};
export default EmployeeDataGrid;
Update client/src/App.tsx with the boilerplate setup below:
import * as React from 'react';
import CssBaseline from '@mui/material/CssBaseline';
import { Container } from '@mui/material';
import EmployeeDataGrid from './components/EmployeeDataGrid';
function App() {
return (
<React.Fragment>
<CssBaseline />
<Container maxWidth="lg" sx={{ py: 4 }}>
<EmployeeDataGrid />
</Container>
</React.Fragment>
);
}
export default App;
What's happening here:
CssBaselineis a Material UI component that applies consistent baseline CSS styles across different browsers, removing default margins and padding, and setting up consistent font rendering.
Replace the boilerplate code in client/src/main.tsx with the following:
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
7. Run the application
Start the server:
cd server
pnpm run dev
In a new terminal, start the client:
cd client
pnpm run dev
The server runs on http://localhost:3001 and the client on http://localhost:5173.
Part two: Data Grid setup
In this section, you'll build out the Grid's data fetching functionality.
All steps that follow take place in the EmployeeDataGrid.tsx component you created in step 6.
8. Define the data structure
Define what your data looks like by creating interfaces that match your server response.
Add the following interfaces below the imports in EmployeeDataGrid.tsx:
interface Employee {
id: number;
name: string;
email: string;
role: string;
department: string;
salary: number;
startDate: string;
}
interface ApiResponse {
data: Employee[];
total: number;
page: number;
pageSize: number;
}
What's happening here:
Employeedefines the structure of each row in your gridApiResponsedefines what the server sends back, including metadata like total count and pagination info- The
totalfield tells the grid how many total rows exist
9. Define the grid columns
Below the interfaces, define how each column should appear and behave:
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', width: 80 },
{ field: 'name', headerName: 'Name', width: 200 },
{ field: 'email', headerName: 'Email', width: 250 },
{ field: 'role', headerName: 'Role', width: 150 },
{ field: 'department', headerName: 'Department', width: 150 },
{ field: 'salary', headerName: 'Salary', width: 120 },
{ field: 'startDate', headerName: 'Start Date', width: 130 },
];
const EmployeeDataGrid = () => { //...
What's happening here:
fieldmaps to the property names in yourEmployeeinterfaceheaderNameis what users see in the column headerwidthsets the initial column width in pixels- Each column automatically supports sorting and filtering
10. Set up GridDataSource
The GridDataSource tells the Grid how to fetch data:
function EmployeeDataGrid() {
const dataSource: GridDataSource = React.useMemo(
() => ({
getRows: async (params: GridGetRowsParams): Promise<GridGetRowsResponse> => {
// You'll implement this function next
},
}),
[],
);
return (
<Box sx={{ height: 600, width: '100%' }}>
<Typography variant="h4" component="h1" gutterBottom>
MUI X Data Grid with the Data Source layer
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Server-side data with pagination, sorting, and filtering.
</Typography>
</Box>
);
}
What's happening here:
getRowsis an async function that the grid calls whenever it needs dataparamscontains all the information about what data the grid needs- Wrap it in
React.useMemoto prevent recreating the function on every render
11. Build the URL parameters
Inside getRows, construct the API call with the grid's current state:
const dataSource: GridDataSource = useMemo(
() => ({
getRows: async (params: GridGetRowsParams): Promise<GridGetRowsResponse> => {
const urlParams = new URLSearchParams({
paginationModel: JSON.stringify(params.paginationModel),
sortModel: JSON.stringify(params.sortModel || []),
filterModel: JSON.stringify(params.filterModel || {}),
});
},
}),
[],
);
What's happening here:
params.paginationModelcontains the pagination stateparams.sortModelcontains which columns are sorted and in what directionparams.filterModelcontains any active filters
12. Make the API call
Fetch the data from your server:
getRows: async (params: GridGetRowsParams): Promise<GridGetRowsResponse> => {
const urlParams = new URLSearchParams({
paginationModel: JSON.stringify(params.paginationModel),
sortModel: JSON.stringify(params.sortModel || []),
filterModel: JSON.stringify(params.filterModel || {}),
});
const response = await fetch(`http://localhost:3001/api/employees?${urlParams.toString()}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: ApiResponse = await response.json();
return {
rows: result.data,
rowCount: result.total,
};
},
13. Return the data to the grid
Return the data in the format the Grid expects:
return {
rows: result.data,
rowCount: result.total,
};
14. Put it all together
Combining steps 10 through 13, the complete Data Source looks like this:
const dataSource: GridDataSource = useMemo(
() => ({
getRows: async (params: GridGetRowsParams): Promise<GridGetRowsResponse> => {
const urlParams = new URLSearchParams({
paginationModel: JSON.stringify(params.paginationModel),
sortModel: JSON.stringify(params.sortModel || []),
filterModel: JSON.stringify(params.filterModel || {}),
});
const response = await fetch(
`http://localhost:3001/api/employees?${urlParams.toString()}`,
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: ApiResponse = await response.json();
return {
rows: result.data,
rowCount: result.total,
};
},
}),
[],
);
15. Render the Data Grid
Finally, render the Grid with your configuration:
<DataGrid
columns={columns}
dataSource={dataSource}
pagination
pageSizeOptions={[5, 10, 25, 100]}
disableRowSelectionOnClick
/>
What's happening here:
columnsdefines your column structuredataSourceprovides your server-side data fetching logicpaginationenables pagination controlspageSizeOptionslets users choose how many rows to see per pagedisableRowSelectionOnClickprevents row selection when clicking cells
16. Add the UI wrapper
Wrap everything in a container:
return (
<Box sx={{ height: 600, width: '100%' }}>
<Typography variant="h4" component="h1" gutterBottom>
Employee Management
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Server-side data with pagination, sorting, and filtering
</Typography>
<DataGrid
columns={columns}
dataSource={dataSource}
pagination
pageSizeOptions={[5, 10, 25, 100]}
disableRowSelectionOnClick
/>
</Box>
);
17. Complete component code
Here is the complete EmployeeDataGrid.tsx component:
import { useMemo } from 'react';
import {
DataGrid,
type GridColDef,
type GridDataSource,
type GridGetRowsParams,
type GridGetRowsResponse,
} from '@mui/x-data-grid';
import { Box, Typography } from '@mui/material';
interface Employee {
id: number;
name: string;
email: string;
role: string;
department: string;
salary: number;
startDate: string;
}
interface ApiResponse {
data: Employee[];
total: number;
page: number;
pageSize: number;
}
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', width: 80 },
{ field: 'name', headerName: 'Name', width: 200 },
{ field: 'email', headerName: 'Email', width: 250 },
{ field: 'role', headerName: 'Role', width: 150 },
{ field: 'department', headerName: 'Department', width: 150 },
{ field: 'salary', headerName: 'Salary', width: 120 },
{ field: 'startDate', headerName: 'Start Date', width: 130 },
];
const EmployeeDataGrid = () => {
const dataSource: GridDataSource = useMemo(
() => ({
getRows: async (params: GridGetRowsParams): Promise<GridGetRowsResponse> => {
const urlParams = new URLSearchParams({
paginationModel: JSON.stringify(params.paginationModel),
sortModel: JSON.stringify(params.sortModel || []),
filterModel: JSON.stringify(params.filterModel || {}),
});
const response = await fetch(
`http://localhost:3001/api/employees?${urlParams.toString()}`,
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: ApiResponse = await response.json();
return {
rows: result.data,
rowCount: result.total,
};
},
}),
[],
);
return (
<Box sx={{ height: 600, width: '100%' }}>
<Typography variant="h4" component="h1" gutterBottom>
MUI X Data Grid with the Data Source layer
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Server-side data with pagination, sorting, and filtering.
</Typography>
<DataGrid
columns={columns}
dataSource={dataSource}
pagination
pageSizeOptions={[5, 10, 25, 100]}
disableRowSelectionOnClick
/>
</Box>
);
};
export default EmployeeDataGrid;
Now your Data Grid should successfully fetch and render the dummy data from your server, and sorting, filtering, and pagination should all work as expected.
Learn more
To learn more about the features covered in this tutorial, check out the server-side data documentation.