How can I help you?
Connect Syncfusion Angular Grid to GraphQL Backend with Apollo
10 Mar 202624 minutes to read
GraphQL is a query language that allows applications to request exactly the data needed, nothing more and nothing less. Unlike traditional REST APIs that return fixed data structures, GraphQL enables the client to specify the shape and content of the response.
Traditional REST APIs and GraphQL differ mainly in the way data is requested and returned: REST APIs expose multiple endpoints that return fixed data structures, often including unnecessary fields and requiring several requests to fetch related data, while GraphQL uses a single endpoint where queries define the exact fields needed, enabling precise responses and allowing related data to be retrieved efficiently in one request. This makes GraphQL especially useful for Angular Grid integration, the reason is data‑centric UI components require well‑structured and selective datasets to support efficient filtering, reduce network calls, and improve overall performance.
Key GraphQL concepts:
- Queries: A query is a request to read data. Queries do not modify data; they only retrieve it.
- Mutations: A mutation is a request to modify data. Mutations create, update, or delete records.
- Resolvers: Each query or mutation is handled by a resolver, which is a function responsible for fetching data or executing an operation. Query resolvers handle read operations, while mutation resolvers handle write operations.
- Schema: Defines the structure of the API. The schema describes available data types, the fields within those types, and the operations that can be executed. Query definitions specify the way data can be retrieved, and mutation definitions specify the way data can be modified.
What is Apollo?
Apollo Server is a widely used GraphQL server that simplifies creating efficient and scalable APIs. It offers a clear structure for defining schemas, handling queries, and connecting data sources, making it a strong choice for building modern GraphQL backend.
Prerequisites
| Software / Package | Recommended version | Purpose |
|---|---|---|
| Node.js | 20.x LTS or later | Runtime |
| npm / yarn / pnpm | npm 11.6.4 (latest) | Package manager |
| Angular CLI | 18.x (latest stable) | Used to create and manage Angular applications. |
| TypeScript | 5.x or later | Server‑side and client‑side type safety. |
Key topics
| # | Topics | Link |
|---|---|---|
| 1 | Setting up and configuring the GraphQL backend using Apollo | View |
| 2 | Set up the Apollo Server | View |
| 3 | Integrating Syncfusion Angular Grid with Apollo GraphQL | View |
| 4 | Perform data operations including filtering, sorting, searching, and paging | View |
| 5 | Perform CRUD operations | View |
| 6 | Run the GraphQL application | View |
| 7 | Explore a complete working sample on GitHub | View |
Setting up the GraphQL backend using Apollo
The Apollo GraphQL backend acts as the primary data layer, handling all queries and mutations required by the Syncfusion Angular Grid.
Step 1: Create the GraphQL server and Install required packages
Before configuring the GraphQL API, a new folder must be created to host the GraphQL server. This folder will contain the server configuration, required dependencies, and sample data used for processing GraphQL queries.
For this guide, a GraphQL server named GridServer is created using Node.js and TypeScript.
Create project folder:
Open a terminal ( for example, an integrated terminal in Visual Studio Code or Windows Command Prompt opened with Win+R, or macOS Terminal launched with Cmd+Space ) and run the following command to create and navigate into the project folder:
mkdir GridServer
cd GridServer
mkdir src
cd srcGridServer folder structure:
The following structure represents the backend GraphQL server implementation. Further details about each file will be explained later.
├── GridServer
│ ├── src
│ │ ├── data.ts
│ │ ├── resolvers.ts
│ │ ├── schema.graphql
│ │ ├── server.ts
│ │ └── types.ts
│ │
│ ├── package.json
│ └── tsconfig.json
Configure TypeScript:
TypeScript configuration tells the compiler to convert TypeScript to JavaScript and sets up the project structure.
Create a new tsconfig.json file in the GridServer folder using the below command:
npx tsc --initReplace (GridServer/tsconfig.json) file content with the following configuration:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Install required packages
To configure the GraphQL server with Apollo, begin by installing the essential Apollo and server‑side packages.
Run the following commands to add them to the project:
npm install graphql @apollo/server @graphql-tools/schema graphql-type-json
npm install -D typescript ts-node @types/node
npm install @syncfusion/ej2-data --save-
graphql– Core GraphQL library used for defining schemas, types, and executing GraphQL operations. -
@apollo/server– The officialApolloServer package used to create and run a standalone GraphQL server. -
@graphql-tools/schema– Helps build executable GraphQL schemas by combining type definitions and resolvers. -
graphql-type-json- Adds support for JSON scalar types in GraphQL schemas, enabling structured JSON fields. -
typescript, ts-node, @types/node- Enables TypeScript development, type checking, and running TypeScript files. -
@syncfusion/ej2-data- Provides data utilities for advanced data operations.
Create sample datasource
After installing the required packages, create a new file named data.ts inside the src folder. This file acts as an in‑memory datasource for the GraphQL server.
[data.ts]
export const expenses= [
{
"expenseId": "EXP202401",
"employeeName": "John Doe",
"employeeEmail": "[email protected]",
"employeeAvatarUrl": "https://randomuser.me/api/portraits/men/12.jpg",
"department": "Finance",
"category": "Travel",
"description": "Client meeting travel expenses",
"amount": 320.50,
"taxPct": 0.18,
"totalAmount": 378.19,
"expenseDate": "2024-05-14T09:22:00Z",
"paymentMethod": "Credit Card",
"currency": "USD",
"reimbursementStatus": "Approved",
"isPolicyCompliant": true,
"tags": ["travel", "client", "priority"]
}
. . . .
. . . .
. . . .
]The GridServer folder is now created, required packages are installed, and a sample data source is configured. The project is ready for defining the GraphQL schema, resolvers, and server configuration.
Step 2: Configuring schema in GraphQL
The GraphQL schema defines the structure of the “expense” data model and the server‑side operations available for performing CRUD actions.
Instructions:
- Create a new schema file (src/schema.graphql) in the GridServer folder.
-
Add type definition for Expense type:
#--- Expense type definition --- type Expense { expenseId: ID! employeeName: String! #include additional fields } -
Add type definition for ExpenseResult type:
# --- Return type for Grid paging --- type ExpenseResult { result: [Expense!]! count: Int! } -
Add type definition for SortInput:
input SortInput { name: String! direction: String! } -
Add type definition for ExpenseInput:
input ExpenseInput { expenseId: ID employeeName: String #include additional fields } -
Add type definition for FilterInput:
input FilterInput { field: String! operator: String! value: String predicate: String matchCase: Boolean } -
Add type definition for DataManagerInput:
# Add additional parameters (e.g., group, aggregates) here if needed input DataManagerInput { skip: Int take: Int requiresCounts: Boolean sorted: [SortInput!] filtered: [FilterInput!] where: JSON search: String params: JSON }For detailed information about DataManagerInput type refer to Configuring Syncfusion DataManagerInput schema
-
Define the Query type to expose the “getExpenses” operation that returns the list of “expenses”:
type Query { getExpenses(datamanager: DataManagerInput): ExpenseResult! } -
Define Mutation types for CRUD operations:
type Mutation { addExpense(value: ExpenseInput!): Expense! updateExpense(key: ID!, keyColumn: String, value: ExpenseInput!): Expense! deleteExpense(key: ID!, keyColumn: String, value: ExpenseInput): Boolean! }Key Parameters Definitions:
- key: The unique identifier (primary key) of the expense to be updated.
- keyColumn: The name of the column containing the unique identifier.
- value: An object containing the created or updated expense details.
Step 3: Configuring Syncfusion DataManagerInput schema
Syncfusion Grid sends all operation details paging, sorting, filtering, and searching as a single request object. GraphQL requires a clear, typed structure to understand these values.
Syncfusion’s DataManager follows a fixed schema when sending operation details from the client. To ensure seamless integration, the GraphQL backend defines a corresponding input type that mirrors this structure.
DataManagerInput serves as the input type that matches the structure of the DataManager request, ensuring that all operation details are correctly received by the GraphQL API.
Purpose: The DataManagerInput schema provides a standard format for delivering Grid operation parameters to the GraphQL server. This structure allows the backend to return only the required records, improving performance, reducing payload size, and enabling efficient data handling.
Here are the details of DataManagerInput parameter type.
| Parameters | Description |
|---|---|
requiresCounts |
If it is “true” then the total count of records will be included in response. |
skip |
Holds the number of records to skip. |
take |
Holds the number of records to take. |
sorted |
Contains details about current sorted column and its direction. |
where |
Contains details about current filter column name and its constraints. |
group |
Contains details about current Grouped column names. |
search |
Contains details about current search data. |
aggregates |
Contains details about aggregate data. |
Use this DataManagerInput in the “getExpenses” query to access the parameters sent from the client, allowing the GraphQL server to handle these operations in a consistent and unified way.
Step 4: GraphQL - Query resolvers
A resolver in GraphQL is a function responsible for fetching the data for a specific field in a GraphQL schema.
When a client sends a GraphQL query, resolvers run behind the scenes to retrieve the requested information from a database, API, or any data source and return it in the format defined by the schema.
Instructions:
- Create a new resolver file (src/resolvers.ts) inside the GridServer folder.
- Import the required data source (e.g., expenses) from the data file.
- Implement the “getExpenses” resolver to handle the logic for the “getExpenses” query defined in the schema.
- Ensure the resolver returns the processed list of “expenses” in the structure specified by the schema.
import { expenses } from './data';
import { DataManager, Query } from '@syncfusion/ej2-data';
import GraphQLJSON from "graphql-type-json";
import { ExpenseRecord } from './types';
export const resolvers = {
JSON: GraphQLJSON,
Query: {
getExpenses: (_: unknown, { datamanager }: { datamanager: DataStateChangeEventArgs }) => {
let data: ExpenseRecord[] = [...expenses];
const query = new Query();
// Apply search, filter, sort, and paging operations as provided by the Grid.
// Operations are applied sequentially: search → filter → sort → paging.
let result = data
let count = result.length
return {
result,
count
};
},
}
}Step 5: GraphQL - Mutation resolvers
Mutations in GraphQL are used to modify data on the server, such as creating, updating, or deleting records.
Previously, the CRUD mutation types were defined in the schema.graphql file. The next step is to implement these mutation actions inside the resolver.ts file.
Instructions:
- Open the (src/resolvers.ts) file.
-
Implement the “addExpense” mutation.
export const resolvers = { JSON: GraphQLJSON, Query: { . . . . . . . . . }, Mutation: { /** * Creates a new expense record. * * @param _ - Unused. Present only to satisfy the GraphQL resolver signature. * @param value - The full expense payload to insert into the dataset. * @returns The newly created `ExpenseRecord`. */ addExpense: (_: unknown, { value }: { value: ExpenseInput }): ExpenseRecord => { expenses.push(value as ExpenseRecord); return value as ExpenseRecord; }, } }addExpense - code breakdown:
Step Purpose Implementation 1. Receive Input Accept “expense” details from the client. Resolver gets valueinaddExpense(_: unknown, { value })2. Insert Expense Add the incoming “expense” to the existing list. Calls expenses.push(value).3. Return Added Expense Provide the newly added “expense” back to the client. Returns the same valueobject. -
Implement the “updateExpense” mutation.
Mutation: { /** * Updates an existing expense by its `expenseId` (provided as `key`). * * @param _ - Unused. Present to satisfy the GraphQL resolver signature. * @param key - The identifier of the expense to update (matches `expenseId`). * @param value - Partial fields to merge into the existing expense (shallow). */ updateExpense: (_: unknown, { key, value }: UpdateExpenseArgs): ExpenseRecord => { const index = expenses.findIndex((item) => item.expenseId === key); if (index === -1) { throw new Error('Expense not found'); } const existing = expenses[index]; const { expenseId: _ignore, ...rest } = value; const updated: ExpenseRecord = { ...existing, ...rest }; expenses[index] = updated; return updated; }, }updateExpense - code breakdown:
Step Purpose Implementation 1. Receive Input Accept the unique key and the partial fields to update. Resolver parameters: key,value.2. Locate Record Find the expense that matches the provided key. const index = expenses.findIndex(item => item.expenseId === key).3. Validate Match Ensure the target expense exists. if (index === -1) throw new Error('Expense not found')4. Apply Updates Create a new merged object and persist it back into the array. const updated = { ...existing, ...rest }; expenses[index] = updated;5. Return Updated Send back the modified expense to the client. return updatedobject with all updates applied. -
Implement the “deleteExpense” mutation.
/** * Deletes an expense by its `expenseId` (provided as `key`). * * @param _ - Unused. Present to satisfy the GraphQL resolver signature. * @param key - The identifier of the expense to delete (matches `expenseId`). * @returns `true` if the record was deleted; otherwise `false`. */ deleteExpense: (_: unknown, { key }: { key: string }): boolean => { const index = expenses.findIndex((item) => item.expenseId === key); if (index === -1) return false; expenses.splice(index, 1); return true; },deleteExpense - code breakdown:
Step Purpose Implementation 1. Receive Key Backend receives only the primary key value from client. Resolver parameters: key2. Locate Index Find the index of the expense matching the provided key. const index = expenses.findIndex(item => item.expenseId === key)3. Validate Existence Ensure a matching record exists before deletion. if (index === -1) return false4. Remove Record Delete the record at the located index from the array. expenses.splice(index, 1)5. Return Status Indicate whether deletion occurred. Returns “true” if deleted, “false” if not found.
Step 6: Setting up the Apollo server
Instructions:
- Create a new (src/server.ts) file. This file serves as the main entry point for the GraphQL backend when using
ApolloServer.- It initializes and configures the GraphQL backend by loading the schema and resolvers, combining them into an executable schema, and starting the
ApolloServer. - This file acts as the central entry point that sets up the entire GraphQL application. It ensures the server runs, listens on a port, and exposes the GraphQL API endpoint.
- It initializes and configures the GraphQL backend by loading the schema and resolvers, combining them into an executable schema, and starting the
-
Follow the steps below to implement the server:
- Load the GraphQL schema: Import the schema.graphql schema file and load its type definitions.
- Load the resolvers: Bring in the resolver functions that implement the behavior for each field in the schema.
- Create an executable GraphQL schema: Combine the schema and resolvers using the predefined makeExecutableSchema function from GraphQL Tools.
-
Start the Apollo GraphQL server: Initialize
ApolloServer, configure it, start it on a port, and expose the GraphQL endpoint URL.
[GridServer/server.ts] import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { readFileSync } from 'fs'; import { join } from 'path'; import { resolvers } from './resolvers'; // Load the GraphQL schema from the schema.graphql file. const typeDefs = readFileSync(join(__dirname, 'schema.graphql'), 'utf8'); // Create an executable GraphQL schema using typeDefs + resolvers. const schema = makeExecutableSchema({ typeDefs, resolvers, }); async function start() { // Initialize a new Apollo Server instance using the executable schema. const server = new ApolloServer({ schema, csrfPrevention: true, // Enable CSRF protection. cache: 'bounded', // Use optimized bounded cache strategy. }); // Define the port on which the server will run (default: 4000). const port = Number(process.env.PORT) || 4000; // Start the standalone Apollo server and listen on the specified port. const { url } = await startStandaloneServer(server, { listen: { port }, }); } // Start the server. start().catch((err) => { process.exit(1); }); -
Update (package.json) Scripts:
{ "scripts": { "start": "ts-node src/server.ts" } }server.ts is added to the start script so the command launches the GraphQL server entry point. This triggers schema loading, resolver binding, and Apollo Server startup.
Now all required GraphQL types, queries, mutations and Apollo Server configuration is now fully implemented.
Integrating Syncfusion Angular Grid with Apollo GraphQL
Open a Visual Code terminal or Command prompt and run the below command.
ng new GridClient
cd GridClientThis creates an Angular application named GridClient, which serves as the base for configuring the Syncfusion Grid and displaying data retrieved from the server.
The integration process begins by installing the required Syncfusion Angular Grid packages before establishing the GraphQL connection.
Step 1: Adding Syncfusion packages
To use Syncfusion® Grid component and Datamanager, install the packages using the below command.
npm install @syncfusion/ej2-angular-grids --save
npm install @syncfusion/ej2-data-
@syncfusion/ej2-angular-grids– required to use the Syncfusion Angular Grid component. -
@syncfusion/ej2-data– Provides data utilities for binding and manipulating Grid data.
Step 2: Including required Syncfusion stylesheets
Once the dependencies are installed, the required CSS files are made available in the (../node_modules/@syncfusion) package directory, and the corresponding CSS references are included in the styles.css file.
[src/styles.css]
@import '../node_modules/@syncfusion/ej2-base/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-buttons/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-calendars/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-dropdowns/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-inputs/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-navigations/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-popups/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-splitbuttons/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-notifications/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-angular-grids/styles/material3.css';For this project, the “Material3” theme is used. A different theme can be selected or the existing theme can be customized based on project requirements. Refer to the Syncfusion Angular Components Appearance documentation to learn more about theming and customization options.
Step 3: Configure GraphQL Adaptor
Integrate the Grid component with the GraphQL server, Syncfusion provides a built‑in GraphQLAdaptor that translates the user interaction into GraphQL requests, enabling efficient communication with GraphQL servers.
What is a GraphQL Adaptor?
An adaptor is a translator between two different systems. The GraphQL adaptor specifically:
- Receives interaction events from the Grid (user clicks Add, Edit, Delete, sorts, filters, etc).
- Converts these actions into GraphQL query or mutation syntax.
- Sends the GraphQL request to the backend GraphQL endpoint.
- Receives the response data from the backend.
- Formats the response back into a structure the Grid understands.
- Updates the grid display with the new data.
The adaptor enables bidirectional communication between the frontend (Grid) and backend (GraphQL server).

The required response format includes:
- result: The list of data to be displayed in the current Grid view.
- count: The total number of records available in the dataset.
The GraphQLAdaptor needs to be configured to the Syncfusion DataManager to convert the user interaction into GraphQL‑compatible requests. To enable this setup, configure the DataManager with the GraphQLAdaptor, specify the GraphQL server’s response format, and define the query. Finally, assign this DataManager instance to the Grid component.
Instructions:
- In the app.component.html file render the Grid component.
[app.component.html] <ejs-grid [dataSource]="expenseManager" > <e-columns> <e-column field="expenseId" headerText="Expense ID" width="130" textAlign="Center" isPrimaryKey="true" ></e-column> <!--Include Additional columns--> </e-columns> </ejs-grid> -
In the app.component.ts file to configure the
DataManagerwith theGraphQLAdaptor.[app.component.ts] @Component({ selector: 'app-root', standalone: true, templateUrl: './app.component.html', styleUrls: ['./app.component.css'], imports: [GridModule], }) public expenseManager!: DataManager; ngOnInit(): void { this.expenseManager = new DataManager({ url: "http://localhost:4000", adaptor: new GraphQLAdaptor({ response: { result: "getExpenses.result", count: "getExpenses.count", }, query: ` query getExpenses($datamanager: DataManagerInput) { getExpenses(datamanager: $datamanager) { count result { expenseId, employeeName # add additional fields } } } `, }), }); }
GraphQL Query Structure Explained in Detail
The Query property is critical for understanding the data flows. Let’s break down each component:
query getExpenses($datamanager: DataManagerInput) {}
Line breakdown:
-
query- GraphQL keyword indicating a read operation. -
getExpenses- Name of the query (must match resolver name with camelCase). -
($datamanager: DataManagerInput)- Parameter declaration.-
$datamanager- Variable name (referenced as $dataManager throughout the query). -
: DataManagerInput- Type specification.
-
getExpenses(datamanager: $datamanager) {}
Line breakdown:
-
getExpenses(...)- Calls the resolver method in backend. -
datamanager: $datamanager- Passes the $dataManager variable to the resolver. - The resolver receives this object and uses it to apply filters, sorts, searches, and pagination.
count
result {
expenseId, employeeName
}
Line breakdown:
-
count- Returns total number of records (used for pagination).- Example: If 150 total expense records exist, count = 150.
- Grid uses this to calculate the number of pages that exist.
-
result- Contains the array of expense records.-
{ ... }- List of fields to return for each record. - Only requested fields are returned (no over-fetching).
-
Response structure example
When the backend executes the query, it returns a JSON response in this exact structure:
{
"data": {
"getExpenses": {
"count": 1500,
"result": [
{
"expenseId": "EXP202400",
"employeeName": "Mia Johnson",
"employeeEmail": "[email protected]",
"employeeAvatarUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBxdWFsaXR5ID0gODAK/",
"department": "Marketing",
"category": "Office Supplies",
"description": "Printer ink cartridges for finance pod",
"receiptUrl": null,
"amount": 1074.27,
"taxPct": 0.0973,
"totalAmount": 1178.8,
"expenseDate": "2025-12-09T00:00:00.000Z",
"paymentMethod": "Corporate Card",
"currency": "EUR - Euro",
"reimbursementStatus": "Approved",
"isPolicyCompliant": true,
"tags": []
},]}
}}Response structure explanation:
| Part | Purpose | Example |
|---|---|---|
data |
Root object returned for every successful GraphQL query. | Always present in successful response. |
getExpenses |
Matches the GraphQL query name; contains paginated expense data. | Contains count and result. |
count |
Total number of records available. | 1 (in this example) |
result |
Array of expenses objects. | [ {…}, {…} ] |
| Each field in result | Matches GraphQL query field names | Field values from database. |
Step 4: Add toolbar with CRUD and search options
The toolbar provides buttons for adding, editing, deleting records, and searching the data.
Instructions:
- Open the app.component.html file.
-
Update the
Gridcomponent to include the Toolbar property with CRUD and search options:<ejs-grid [dataSource]="expenseManager" [toolbar]="['Add', 'Edit', 'Delete', 'Search']" > <e-columns> <e-column field="expenseId" headerText="Expense ID" width="130" textAlign="Center" isPrimaryKey="true"></e-column> <!--Include Additional columns--> </e-columns> </ejs-grid> -
Open the app.component.ts file, inject the
ToolbarService,EditServiceto the providers.import { GridModule, ToolbarService } from '@syncfusion/ej2-angular-grids'; @Component({ selector: 'app-root', standalone: true, templateUrl: './app.component.html', styleUrls: ['./app.component.css'], imports: [GridModule], providers: [ToolbarService] })
Step 5: Implement paging feature
Paging divides large datasets into smaller pages to improve performance and usability.
During pagination, the GraphQLAdaptor sends the paging details through “skip” and “take” parameters of the “DataManagerInput”. These details are converted to the paging query and passed to the DataManager ensuring that data is returned in paged segments and allowing smooth navigation through large datasets.
Instructions:
-
Set the allowPaging property “true” to enable paging in the Grid.
<ejs-grid [dataSource]="expenseManager" [allowPaging]="true" > <e-columns> <e-column field="expenseId" headerText="Expense ID" width="130" textAlign="Center" isPrimaryKey="true"></e-column> <!--Include Additional columns--> </e-columns> </ejs-grid> -
Open the app.component.ts file, inject the
PageService, and define thepageSettingsconfiguration.import { GridModule, PageService } from '@syncfusion/ej2-angular-grids'; @Component({ selector: 'app-root', standalone: true, templateUrl: './app.component.html', styleUrls: ['./app.component.css'], imports: [GridModule], providers: [PageService] }) public pageSettings = { pageSize: 20, pageSizes: true }; -
Implement the page logic within the “getExpenses” resolver function located in the resolver.ts file.
import { expenses} from './data'; import { DataManager, Query } from '@syncfusion/ej2-data'; import GraphQLJSON from "graphql-type-json"; import { ExpenseRecord } from './types'; const resolvers = { JSON: GraphQLJSON, Query: { getExpenses: (_: unknown, { datamanager }: GetExpensesArgs) => { let data: ExpenseRecord[] = [...expenses]; const query = new Query(); function performPaging( data: ExpenseRecord[], datamanager?: DataStateChangeEventArgs): ExpenseRecord[] { if ( typeof datamanager?.skip === 'number' && typeof datamanager?.take === 'number' ) { const pageQuery = new Query().page( datamanager.skip / datamanager.take + 1, datamanager.take ); return new DataManager(data).executeLocal(pageQuery) as ExpenseRecord[]; } if (typeof datamanager?.take === 'number') { const pageQuery = new Query().page(1, datamanager.take); return new DataManager(data).executeLocal(pageQuery) as ExpenseRecord[]; } return data; } let result = performPaging(data, datamanager); return { result, count }; }, }, }; export default resolvers;Page logic breakdown:
Part Purpose datamanager.skipJSON‑stringified array of search instructions sent by the Grid via GraphQLAdaptor. datamanager.takeParses the JSON and applies the search to the Query. performPaging(data, datamanager)Applies paging to the Query.
Paging details included in request payloads:
The image illustrates the paging details (skip and take) included in the server request payload.

The resolver processes the Grid’s skip and take parameters and returns the total count along with the paged result. Paging feature is now active with “20” records per page.
To use Row Virtualization, inject the
VirtualScrollservice and set enableVirtualization property to “true”. When virtualization is enabled, the grid automatically sends the correct “skip” and “take” values to the server.The resolver does not require any additional modifications. The Grid inherently handles all virtual block requests, ensuring the expected behavior without additional configuration.
Step 6: Implement searching feature
The toolbar provides buttons for searching the data.
When a search action is performed in the Grid, the GraphQLAdaptor sends the search key and the target fields through the “search” parameter of the “DataManagerInput”. These values are converted as the search query and processed through the DataManager.
Instructions:
-
Enable searching in the Grid, add the
Searchin the Grid’stoolbaritems.<ejs-grid [dataSource]="expenseManager" [allowFiltering]="true" [toolbar]="['Search']" > <e-columns> <e-column field="expenseId" headerText="Expense ID" width="130" textAlign="Center" isPrimaryKey="true"></e-column> <!--Include Additional columns--> </e-columns> </ejs-grid> -
Implement the search logic within the “getExpenses” resolver function located in the resolver.ts file.
import { expenses } from './data'; import { DataManager, Query } from '@syncfusion/ej2-data'; import GraphQLJSON from 'graphql-type-json'; import { ExpenseRecord } from './types'; export const resolvers = { JSON: GraphQLJSON, Query: { getExpenses: (_: unknown, { datamanager }: { datamanager: DataStateChangeEventArgs }) => { let data: ExpenseRecord[] = [...expenses]; /* Create Syncfusion Query instance */ const query = new Query(); /** * -------------------------------------------------------------------- * performSearching() * -------------------------------------------------------------------- * custom function that reads `datamanager.search`, * parses it using parseArg(), */ function performSearching(query: Query, datamanager?: DataStateChangeEventArgs){ const searchArg = parseArg<SearchSettingsModel>(datamanager?.search as SearchSettingsModel); if (Array.isArray(searchArg) && searchArg.length) { const { fields, key, operator, ignoreCase } = searchArg[0]; if (key && Array.isArray(fields) && fields.length) { query.search(key, fields, operator, ignoreCase); } } } performSearching(query, datamanager); let result = new DataManager(data).executeLocal(query) as ExpenseRecord[]; const count = data.length; return { result, count }; }, } }Search logic breakdown:
Part Purpose datamanager.searchJSON‑serialized array of searchinstructions sent by the Grid via GraphQLAdaptor.performSearching(searchParam)Parses the JSON and applies the searchto the query.query.search(key, fields)Applies Syncfusion’s contains search across the specified fields. executeLocal(query)Runs the search against the in‑memory expenses collection.
Searching details included in request payloads:
The image below displays the “search” parameter values.

Step 7: Implement sorting feature
Sorting allows the user to organize records by clicking on column headers to arrange data in ascending or descending order.
The GraphQLAdaptor automatically passes the sorting details to the server through the “sorted” parameter of the “DataManagerInput” and the details are converted to the sorting query and executed through the DataManager to get the sorted data.
Instructions:
-
To enable sorting in the Grid, set the allowSorting property to “true”.
<ejs-grid [dataSource]="expenseManager" [allowSorting]="true" > <e-columns> <e-column field="expenseId" headerText="Expense ID" width="130" textAlign="Center" isPrimaryKey="true"></e-column> <!--Include Additional columns--> </e-columns> </ejs-grid> -
Open the app.component.ts file, inject the
SortService, to the provider.import { GridModule, SortService } from '@syncfusion/ej2-angular-grids'; @Component({ selector: 'app-root', standalone: true, templateUrl: './app.component.html', styleUrls: ['./app.component.css'], imports: [GridModule], providers: [SortService] }) -
Implement the sort logic within the “getExpenses” resolver function located in the resolver.ts file.
import { expenses } from './data'; import { DataManager, Query } from '@syncfusion/ej2-data'; import GraphQLJSON from 'graphql-type-json'; import { ExpenseRecord } from './types'; export const resolvers = { JSON: GraphQLJSON, Query: { getExpenses: (_: unknown, { datamanager }: { datamanager: DataStateChangeEventArgs }) => { let data: ExpenseRecord[] = [...expenses]; const query = new Query(); function performSorting(query: Query, datamanager: DataStateChangeEventArgs) { const sortedArg = datamanager?.sorted; if (Array.isArray(sortedArg)) { sortedArg.forEach(({ name, direction }) => { query.sortBy(name, direction); }); } } performSorting(query, datamanager); let result = new DataManager(data).executeLocal(query) as ExpenseRecord[]; const count = result.length; return { result, count }; }, }, };Sorting logic breakdown:
Part Purpose datamanager.sortedArray of sortinstructions sent by the Grid via GraphQLAdaptor.performSorting(sorted)Iterates the sortarray and appends sort clauses to the query.query.sortBy(name, direction)Field/column name to sort by (e.g., “employeeName”, “category”).
Sorting details included in request payloads:
The image below shows the values passed to the “sorted” parameter.

Step 8: Implement filtering feature
Filtering allows the user to narrow down records by specifying conditions on column values. Users can filter by selecting checkbox filters or using comparison operators like equals, greater than, less than, etc.
The GraphQLAdaptor automatically passes the filter conditions to the server through the “where” parameter of the “DataManagerInput”. In the server, the filter parameters are converted to the Syncfusion filter query and executed through the DataManager to get the filtered data.
Instructions:
-
Enable the Grid with filtering by setting the allowFiltering property to
true.<ejs-grid [dataSource]="expenseManager" [allowFiltering]="true" [filterSettings]="{ type: 'CheckBox' }" > <e-columns> <e-column field="expenseId" headerText="Expense ID" width="130" textAlign="Center" isPrimaryKey="true"></e-column> <!--Include Additional columns--> </e-columns> </ejs-grid> -
Open the app.component.ts file, inject the
FilterService, to the provider.import { GridModule, FilterService } from '@syncfusion/ej2-angular-grids'; @Component({ selector: 'app-root', standalone: true, templateUrl: './app.component.html', styleUrls: ['./app.component.css'], imports: [GridModule], providers: [FilterService] }) -
Implement the filter processing logic within the “getExpenses” resolver function located in the resolver.ts file.
import { DataManager, Query, Predicate } from '@syncfusion/ej2-data'; import { expenses } from './data'; import GraphQLJSON from 'graphql-type-json'; import { ExpenseRecord } from './types'; export const resolvers = { JSON: GraphQLJSON, Query: { getExpenses: (_: unknown, { datamanager }: { datamanager: DataStateChangeEventArgs }) => { let data: ExpenseRecord[] = [...expenses]; const query = new Query(); performFiltering(query, datamanager); let result = new DataManager(data).executeLocal(query) as ExpenseRecord[]; const count = data.length; return { result, count }; }, }, },The “performFiltering” function processes the filtering rules received from the client and return the filter query.
/* ------------------------------------------------------------- * performFiltering() * ------------------------------------------------------------- * Reads the "where" filter argument (string or object), * parses it, builds Syncfusion Predicate objects, and applies */ function performFiltering(query: Query, datamanager: DataStateChangeEventArgs) { const whereArg = parseArg<Predicate[]>(datamanager?.where as Predicate[]); /* If valid where conditions exist, build predicates */ if (Array.isArray(whereArg) && whereArg.length) { const rootPredicate = buildPredicate(whereArg[0]); /* Apply predicate to query */ if (rootPredicate) { query.where(rootPredicate); } } }The “parseArg” safely converts a JSON string into an object, returning the original value when it is already an object.
/** * ------------------------------------------------------------- * parseArg() * ------------------------------------------------------------- * Safely parses a JSON string into an object. * If arg is already an object, returns as-is. */ function parseArg<T>(arg?: string | T): T | undefined { if (arg === undefined || arg === null) return undefined; if (typeof arg === 'string') { try { return JSON.parse(arg); } catch { return undefined; } } return arg; }The “buildPredicate” function handles complex filtering scenarios containing nested structures.
/** * ------------------------------------------------------------- * buildPredicate() * ------------------------------------------------------------- * Recursively converts complex filter structures into * Syncfusion Predicate objects. */ function buildPredicate(predicate: Predicate): Predicate | null { if (!predicate) return null; if (predicate.isComplex && Array.isArray(predicate.predicates)) { const children = predicate.predicates .map((child) => buildPredicate(child)) .filter((p): p is Predicate => Boolean(p)); if (!children.length) return null; return children.reduce((acc, curr, idx) => { if (idx === 0) return curr; return predicate.condition?.toLowerCase() === 'or' ? acc.or(curr) : acc.and(curr); }); } if (predicate.field) { return new Predicate( predicate.field, predicate.operator, predicate.value, predicate.ignoreCase, predicate.ignoreAccent ); } return null; }Filter Logic Breakdown:
Part Purpose dataManager.WhereList of filter conditions from the Grid. parseArg(datamanager?.where)Safely parses where into an object/array; returns it as‑is if already an object; returns undefined on invalid JSON. whereArgThe parsed value from “parseArg”; expected to be an array of filter predicates. buildPredicate(predicate)Recursively converts the filter block/tree into a Syncfusion Predicate. predicate.isComplexIndicates a predicate that contains nested predicates which must be processed recursively. predicate.predicates[]The list of child predicates in a complex predicated. Each child can itself be a complex group or a simple predicate. predicate.conditionLogical combiner for children: AND or OR. predicate.fieldColumn/field name to filter (e.g., “employeeName”, “category”, “employeeEmail”). predicate.operatorOperator string (equal, contains, greaterthan, etc.) passed into new Predicate(…). performFiltering(filterString)Parses the where string, builds a combined Predicate chain (AND/OR), and applies it to query via query.where(combinedPredicate). query.where(rootPredicate)Applies the final (combined) Predicate to the Syncfusion Query when a root predicate exists.
Supported Filter Operators:
| Operator | Purpose | Example |
|---|---|---|
equal |
Exact match | Amount equals “500” |
notequal |
Not equal to value | Status not equal to “Rejected” |
contains |
Contains substring (case-insensitive) | Description contains “travel” |
startswith |
Starts with value | EmployeeName starts with “John” |
endswith |
Ends with value | Category ends with “Supplies” |
greaterthan |
Greater than numeric value | Amount > 1000 |
lessthan |
Less than numeric value | TaxPct < 0.15 |
greaterthanequal |
Greater than or equal | Amount >= 500 |
lessthanequal |
Less than or equal | TaxPct <= 0.10 |
Filtering details included in request payloads:
The image illustrates the serialized “where” condition passed from the DataManager.

Filter Logic with Multiple Checkbox Selections:
When a user selects multiple checkbox values for the same column (e.g., (category = “Training & Education” OR category = “Office Supplies”)), the Grid sends a nested predicate block where all selected values are combined using OR logic.
- Top‑level predicates across different fields are combined using AND logic
- Nested predicates within the same field are combined using OR logic
- This enables expressions such as:(category = “Training & Education” OR category = “Office Supplies”) (department = “Marketing” OR department = “Sales”)
The backend GraphQL resolver receives this nested structure through the where parameter and processes it using the recursive “buildNestedPredicate” function, allowing it to handle multi‑level AND/OR combinations for fields like category, employeeEmail, amount, and other expense attributes.
Perform CRUD operations
CRUD operations (Create, Read, Update, Delete) allow users to manage data through the Grid. The Grid provides built-in dialogs and buttons to perform these operations, while the backend resolvers handle the actual data modifications.
To enable editing features in the Syncfusion Angular Grid, configure the Grid’s editSettings and provide the required services ToolbarService and EditService in the component metadata.
[app.component.html]
<ejs-grid
[dataSource]="expenseManager"
[toolbar]="toolbar"
[editSettings]="editSettings">
<e-columns>
<e-column
field="expenseId"
headerText="Expense ID"
width="130"
textAlign="Center"
isPrimaryKey="true"></e-column>
<!--Include Additional columns-->
</e-columns>
</ejs-grid> [app.component.ts]
@Component({
selector: "app-root",
standalone: true,
imports: [ GridModule ],
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
providers: [
ToolbarService,
EditService,
],
})
export class AppComponent {
public toolbar: string[] = ["Add", "Edit", "Delete", "Search"];
//Enable Editing
public editSettings = {
allowEditing: true,
allowAdding: true,
allowDeleting: true,
mode: "Dialog",
};
}The getMutation function in the GraphQLAdaptor handles the Grid CRUD actions by sending the appropriate mutation for each action (insert, update, or delete) to the GraphQL server.
Previously, the required mutation definitions and schema for CRUD operations were created in the resolver.ts and schema.graphql files. The next step is to enable CRUD actions in the client Data Grid by using the GraphQL adaptor.
Insert:
The Insert operation enables adding new “expense” records to the existing list. When the Add button in the toolbar is selected, the Grid opens a dialog that displays input fields for entering “expense” details.
After the required data is submitted, the GraphQL mutation sends the new “expense” record to the backend for processing and storage.
Open the app.component.ts and Configure the getMutation function in the GraphQLAdaptor to return the appropriate GraphQL mutation query string based on the insert action.
[app.component.ts]
// mutation to perform insert
this.expenseManager = new DataManager({
url: "http://localhost:4000",
adaptor: new GraphQLAdaptor({
response: {
result: "getExpenses.result",
count: "getExpenses.count",
},
query: `
query getExpenses($datamanager: DataManagerInput) {
getExpenses(datamanager: $datamanager) {
count
result {
expenseId, employeeName # add additional fields
}
}
}
`,
getMutation: (action: string) => {
if (action === "insert") {
return `
mutation addExpense($value: ExpenseInput!) {
addExpense(value: $value) {
expenseId, employeeName # add additional fields
}
}
`;
}
}),
});Insert mutation request parameters:
When the user clicks the Add button, fills the dialog, and submits, the GraphQLAdaptor constructs the mutation with these parameters:

Update:
The Update operation enables editing of existing “expense” records. When the Edit option in the toolbar is selected and a row is chosen, the Grid opens a dialog displaying the current values of the selected record.
After the required modifications are submitted, a GraphQL mutation sends the updated record to the backend for processing.
Open the app.component.ts file Configure the getMutation function in the GraphQLAdaptor to return the appropriate GraphQL mutation based on the update action which reference the “updateExpense” mutation defined in the schema.
[app.component.ts]
// mutation to perform update
this.expenseManager = new DataManager({
url: "http://localhost:4000",
adaptor: new GraphQLAdaptor({
response: {
result: "getExpenses.result",
count: "getExpenses.count",
},
query: `
query getExpenses($datamanager: DataManagerInput) {
getExpenses(datamanager: $datamanager) {
count
result {
expenseId, employeeName # add additional fields
}
}
}
`,
getMutation: (action: string) => {
if (action === "update") {
return `
mutation updateExpense($key: ID!, $keyColumn: String, $value: ExpenseInput!) {
updateExpense(key: $key, keyColumn: $keyColumn, value: $value) {
expenseId
}
}
`;
}
},
}),
});Update mutation request parameters:
When the user clicks the Edit button, modifies the dialog, and submits, the GraphQLAdaptor constructs the mutation with these parameters:

Delete:
The Delete operation enables removal of “expense” records from the application. When the Delete option in the toolbar is selected and a row is marked for removal, a confirmation prompt appears. After confirmation, a GraphQL mutation sends a delete request to the backend containing only the primary key value.
Open the app.component.ts and Configure the getMutation function in the GraphQLAdaptor to return the delete mutation that matches the “deleteExpense” mutation defined in the schema.
[app.component.ts]
// mutation to perform delete
this.expenseManager = new DataManager({
url: "http://localhost:4000",
adaptor: new GraphQLAdaptor({
response: {
result: "getExpenses.result",
count: "getExpenses.count",
},
query: `
query getExpenses($datamanager: DataManagerInput) {
getExpenses(datamanager: $datamanager) {
count
result {
expenseId, employeeName # add additional fields
}
}
}
`,
getMutation: (action: string) => {
if (action === "remove") {
return `
mutation deleteExpense($key: ID!, $keyColumn: String, $value: ExpenseInput) {
deleteExpense(key: $key, keyColumn: $keyColumn, value: $value)
}
`;
}
},
}),
});Delete mutation request parameters:
When the user clicks the Delete button, selects a row to delete, and confirms the deletion, the GraphQLAdaptor constructs the mutation with minimal parameters:

Normal/Inline editing is the default edit mode for the Grid component. To enable CRUD operations, ensure that the isPrimaryKey property is set to “true” for a specific Grid Column, ensuring that its value is unique.
Running the application
Open a terminal or Command Prompt. Run the server application first then start the client application.
Run the GraphQL server
- Run the following commands to start the server:
cd GridServer
npm start- The server is now running at http://localhost:4000.
Run the client
- Execute the below commands run the client application:
cd GridClient
ng serve- Open http://localhost:5173 in a browser.
The complete folder structure is as follows:
syncfusion-angular-grid-with-apollo-server-/
│
├── GridClient
│ └── src
│ ├── app
│ ├── index.html
│ ├── main.ts
│ ├── styles.css
│ ├── package.json
│ └── tsconfig.json
│
├── GridServer
│ ├── src
│ │ ├── data.ts
│ │ ├── resolvers.ts
│ │ ├── schema.graphql
│ │ ├── server.ts
│ │ └── types.ts
│ │
│ ├── package.json
│ └── tsconfig.json
│
└── README.md
Complete Sample Repository
For a complete working implementation of this example, refer to the following GitHub repository.
This guide provides a modern, high‑performance architecture in which the Syncfusion Angular Grid integrates seamlessly with an Apollo‑powered GraphQL backend.