AG Grid Server-Side Row Model without writing a backend

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.

6 min readAugust 10, 2026Radu

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
IServerSideGetRowsRequest
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.
In 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:
POST /{dataset}/query
with a filter tree, sort model, group/pivot descriptors and a drill path — get back flat rows, group nodes or pivot trees.
npm install @thedatagrid/client
, call
createAgGridServerSideDatasource()
, 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.

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
getRows
call with a request like this:
{
  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" }],
}
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
POST /{dataset}/query
accepting a request that carries the same information in a grid-agnostic shape — a
filter
predicate tree,
sort
,
groupBy
+
groupKeys
,
pivotBy
,
aggregations
— 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:
curl -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" }]
  }'
{
  "protocol": "tdgp/1",
  "data": [
    {
      "keys": ["Argentina"],
      "data": { "country": "Argentina" },
      "aggregations": { "avgSalary": 121043.5 }
    }
  ],
  "totalCount": 24
}
The server at
data.thedatagrid.com
is public, CORS-open and needs no auth — everything in this post runs against it, straight from your browser.

Wiring AG Grid to TDGP#

The
@thedatagrid/client
package ships the protocol client plus an AG Grid binding that does the request/response translation. The whole integration:
import { 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} />;
That's it — no
getRows
implementation, no filter translation, no group bookkeeping. The binding converts each
IServerSideGetRowsRequest
into a TDGP query and each TDGP response into the
success()
call AG Grid expects (including
pivotResultFields
when pivoting). It's also dependency-free: the AG Grid types are structural, so the package doesn't pull in
ag-grid-*
itself.

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
developers-10k
dataset. Everything you do in this grid becomes a TDGP query; open your browser's network tab to watch the conversation.
Things to try:
  • Expand groups — each expansion is a lazy query with that group's
    groupKeys
    drill path; leaf rows only load at full depth.
  • Enable pivot mode in the columns tool panel and pivot on
    Language
    or
    city
    — the server computes the pivot tree and the binding turns it into AG Grid's secondary columns.
  • 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
import { 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 (
text
,
number
,
date
and
set
filters, with their
AND
/
OR
combinators) becomes a TDGP predicate tree —
group
,
not
and
predicate
nodes with operators like
EQ
,
BETWEEN
,
IN
,
CONTAINS
,
IS_BLANK
. The tree is deliberately expressive enough to round-trip AG Grid's filters without loss.
Lazy grouping.
rowGroupCols
+
groupKeys
map to TDGP's
groupBy
+
groupKeys
. When the drill path reaches full depth (
groupKeys.length === groupBy.length
), 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.
Pivoting. In pivot mode,
pivotCols
become TDGP's
pivotBy
, and the nested pivot-cell tree in the response gets flattened into the
pivotResultFields
AG Grid uses to build secondary columns — respecting AG Grid's
serverSidePivotResultFieldSeparator
.
Errors. TDGP servers never silently drop an unsupported feature — you get a typed error envelope (
UNSUPPORTED_FEATURE
,
FIELD_NOT_FOUND
, ...) so the grid can fail loudly instead of rendering wrong data.

One 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
DataSource
through
@thedatagrid/client/infinite
, and anything else through the plain client. Implement TDGP once over your own data — validate with
@thedatagrid/protocol
, shape your store behind a
DataAdapter
with
@thedatagrid/protocol-runtime
, prove it with
@thedatagrid/protocol-testkit
— and the grid becomes a swappable frontend decision.
Beyond queries, the protocol also covers filter-value discovery (Excel-style cascading filter dropdowns via
POST /{dataset}/filter-values
), live updates over server-sent events (try
curl -N https://data.thedatagrid.com/quotes/stream
), a dataset catalog with per-field filter capabilities, and an OpenAPI spec generated from the live server.
The 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