AG Grid Server-Side Row Model without writing a backend

The AG Grid Server-Side Row Model needs a query backend that most teams end up writing from scratch. We wire AG Grid to THE DataGrid Protocol instead — server-side grouping, pivoting, filtering and pagination against a live public server, in about ten lines of integration code.
AG Grid Server-Side Row Model without writing a backend#
The Server-Side Row Model (SSRM) is the feature that makes AG Grid viable for really large datasets: instead of shipping every row to the browser, the grid asks the server for exactly the slice it needs — filtered, sorted, grouped, pivoted, one block at a time.
There's a catch, though. AG Grid defines the client half of that conversation — the your datasource receives — and leaves the server half entirely to you. Every team building on SSRM ends up writing the same backend: translate the filter model to SQL, implement lazy group expansion, recompute aggregations per level, handle pivoting. It's a lot of non-trivial code, and it's grid-specific — switch grids and you write it again.
IServerSideGetRowsRequestIn this article we take a different route: wire AG Grid's SSRM to THE DataGrid Protocol (TDGP) — an open, versioned JSON protocol for exactly this conversation — and run it against a live public server at data.thedatagrid.com. The full integration is about ten lines.
TL;DR
TDGP standardizes the grid-to-backend contract: with a filter tree, sort model, group/pivot descriptors and a drill path — get back flat rows, group nodes or pivot trees. , call , and AG Grid's SSRM does server-side grouping, pivoting, filtering, sorting and pagination with no backend code of your own. Full protocol reference at /protocol.
POST /{dataset}/querynpm install @thedatagrid/clientcreateAgGridServerSideDatasource()The conversation SSRM wants to have#
When AG Grid runs in server-side mode, every scroll, group expansion, sort or filter change turns into a call with a request like this:
getRows{
startRow: 0,
endRow: 100,
rowGroupCols: [{ id: "country", field: "country" }],
valueCols: [{ id: "salary", field: "salary", aggFunc: "sum" }],
pivotCols: [],
pivotMode: false,
groupKeys: [], // [] = top-level groups; ["France"] = France's children
filterModel: { age: { filterType: "number", type: "greaterThan", filter: 30 } },
sortModel: [{ colId: "salary", sort: "desc" }],
} COPY
Your job is to answer it: the right page of rows, or group nodes with aggregations, or a pivoted result — and to keep answering correctly as the user drills into groups, stacks filters and toggles pivot mode.
TDGP is a protocol-shaped answer to that job. A TDGP server exposes accepting a request that carries the same information in a grid-agnostic shape — a predicate tree, , + , , — and responds with one of three well-defined shapes: flat rows, group nodes, or group nodes with nested pivot cells. Here's the wire-level version of "group 10,000 developers by country, average salary per group", against the live server:
POST /{dataset}/queryfiltersortgroupBygroupKeyspivotByaggregationscurl -X POST https://data.thedatagrid.com/developers-10k/query \
-H 'content-type: application/json' \
-d '{
"process": { "group": "server" },
"groupBy": [{ "field": "country" }],
"groupKeys": [],
"aggregations": [{ "id": "avgSalary", "field": "salary", "fn": "avg" }]
}' COPY
{
"protocol": "tdgp/1",
"data": [
{
"keys": ["Argentina"],
"data": { "country": "Argentina" },
"aggregations": { "avgSalary": 121043.5 }
}
],
"totalCount": 24
} COPY
The server at is public, CORS-open and needs no auth — everything in this post runs against it, straight from your browser.
data.thedatagrid.comWiring AG Grid to TDGP#
The package ships the protocol client plus an AG Grid binding that does the request/response translation. The whole integration:
@thedatagrid/clientimport { createTdgpClient } from "@thedatagrid/client";
import { createAgGridServerSideDatasource } from "@thedatagrid/client/ag-grid";
const client = createTdgpClient({ url: "https://data.thedatagrid.com" });
const serverSideDatasource = createAgGridServerSideDatasource({
client,
dataset: "developers-10k",
});
<AgGridReact rowModelType="serverSide" serverSideDatasource={serverSideDatasource} />; COPY
That's it — no implementation, no filter translation, no group bookkeeping. The binding converts each into a TDGP query and each TDGP response into the call AG Grid expects (including when pivoting). It's also dependency-free: the AG Grid types are structural, so the package doesn't pull in itself.
getRowsIServerSideGetRowsRequestsuccess()pivotResultFieldsag-grid-*The live demo#
Here's the full thing running — server-side row grouping by country and stack, aggregations (average age, total salary, total repos), number filters, sorting and pagination over the dataset. Everything you do in this grid becomes a TDGP query; open your browser's network tab to watch the conversation.
developers-10kThings to try:
- Expand groups — each expansion is a lazy query with that group's drill path; leaf rows only load at full depth.
groupKeys - Enable pivot mode in the columns tool panel and pivot on or
Language— the server computes the pivot tree and the binding turns it into AG Grid's secondary columns.city - Stack filters — number filters on age/salary/repos combine into a single predicate tree, evaluated server-side. Note the group aggregations recompute under the filter.
- Sort by an aggregation — sort the salary column while grouped: group nodes are ordered by their aggregated value, on the server.
AG Grid SSRM backed by THE DataGrid Protocol
View Mode
Fork Forkimport { AgGridReact } from "ag-grid-react"; import { AllEnterpriseModule, ColDef, GridOptions, IServerSideDatasource, ModuleRegistry, ValueFormatterParams, themeQuartz, colorSchemeDark, } from "ag-grid-enterprise"; import { createTdgpClient } from "@thedatagrid/client"; import { createAgGridServerSideDatasource } from "@thedatagrid/client/ag-grid"; ModuleRegistry.registerModules([AllEnterpriseModule]); type Developer = { id: number; firstName: string; lastName: string; country: string; city: string; stack: string; preferredLanguage: string; age: number; salary: number; reposCount: number; }; // A public TDGP server — CORS-open, no auth needed const client = createTdgpClient({ url: "https://data.thedatagrid.com" }); // The entire "backend integration" for AG Grid SSRM: const serverSideDatasource = createAgGridServerSideDatasource({ client, dataset: "developers-10k", }) as IServerSideDatasource; const currencyFormatter = (params: ValueFormatterParams<Developer>) => params.value == null ? "" : `$${Number(params.value).toLocaleString()}`; const columnDefs: ColDef<Developer>[] = [ { field: "country", rowGroup: true, hide: true, enableRowGroup: true, enablePivot: true, }, { field: "stack", rowGroup: true, hide: true, enableRowGroup: true, enablePivot: true, }, { field: "city", enableRowGroup: true, enablePivot: true }, { field: "firstName" }, { field: "lastName" }, { field: "preferredLanguage", headerName: "Language", enableRowGroup: true, enablePivot: true, }, { field: "age", aggFunc: "avg", enableValue: true, filter: "agNumberColumnFilter", }, { field: "salary", aggFunc: "sum", enableValue: true, filter: "agNumberColumnFilter", valueFormatter: currencyFormatter, }, { field: "reposCount", headerName: "Repos", aggFunc: "sum", enableValue: true, filter: "agNumberColumnFilter", }, ]; const gridOptions: GridOptions<Developer> = { theme: themeQuartz.withPart(colorSchemeDark), columnDefs, defaultColDef: { flex: 1, minWidth: 120, sortable: true, filter: true, resizable: true, }, autoGroupColumnDef: { headerName: "Country / Stack", minWidth: 220, }, rowModelType: "serverSide", serverSideDatasource, pagination: true, paginationPageSize: 100, cacheBlockSize: 100, animateRows: true, sideBar: { toolPanels: ["columns", "filters"] }, }; export default function () { return <AgGridReact gridOptions={gridOptions} />; }
What the binding actually translates#
A few of the mappings worth knowing about, because they're where hand-rolled SSRM backends usually accumulate bugs:
Filter models. AG Grid's per-column filter model (, , and filters, with their / combinators) becomes a TDGP predicate tree — , and nodes with operators like , , , , . The tree is deliberately expressive enough to round-trip AG Grid's filters without loss.
textnumberdatesetANDORgroupnotpredicateEQBETWEENINCONTAINSIS_BLANKLazy grouping. + map to TDGP's + . When the drill path reaches full depth (), the server pins the path as equality filters and returns leaf rows. Aggregations are recomputed per level — the protocol is explicit that an average of averages is not an average.
rowGroupColsgroupKeysgroupBygroupKeysgroupKeys.length === groupBy.lengthPivoting. In pivot mode, become TDGP's , and the nested pivot-cell tree in the response gets flattened into the AG Grid uses to build secondary columns — respecting AG Grid's .
pivotColspivotBypivotResultFieldsserverSidePivotResultFieldSeparatorErrors. TDGP servers never silently drop an unsupported feature — you get a typed error envelope (, , ...) so the grid can fail loudly instead of rendering wrong data.
UNSUPPORTED_FEATUREFIELD_NOT_FOUNDOne protocol, any grid#
The interesting part of doing this with a protocol rather than a bespoke backend: the server we just used has no idea AG Grid exists. The same endpoint serves Infinite Table's lazy through , and anything else through the plain client. Implement TDGP once over your own data — validate with , shape your store behind a with , prove it with — and the grid becomes a swappable frontend decision.
DataSource@thedatagrid/client/infinite@thedatagrid/protocolDataAdapter@thedatagrid/protocol-runtime@thedatagrid/protocol-testkitBeyond queries, the protocol also covers filter-value discovery (Excel-style cascading filter dropdowns via ), live updates over server-sent events (try ), a dataset catalog with per-field filter capabilities, and an OpenAPI spec generated from the live server.
POST /{dataset}/filter-valuescurl -N https://data.thedatagrid.com/quotes/streamThe full contract — endpoints, request/response shapes, the FilterModel, error codes, conformance levels, versioning rules, and how to implement a server — is documented at /protocol.
Help shape TDGP
Want help adopting TDGP against your own store or grid? Have an idea for an extension, a new binding, or a conformance level? Use the contact form — we're actively looking for partners to support and expand the protocol.
For more content like this, follow us on at @thedatagrid