Live REST (public API)
Real rows over the network from dummyjson.com via the enterprise createRestDataSource + a shape adapter (dummyJsonAdapter): skip/limit paging and sortBy/order sorting mapped to the API dialect. Swap URL + adapter (jsonServerAdapter / offsetLimitAdapter) to point at any public API. Includes an error/retry surface. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Server-Side Row Model). See the SvGrid documentation for the full API.
About this example
Real rows over the network in the Svelte 5 data grid, with no mock and no seeded array. The grid talks to the public dummyjson.com products API through the enterprise createRestDataSource, and a shape adapter, dummyJsonAdapter, teaches it that API's dialect: skip and limit paging, sortBy and order sorting, rows under products with the count in total. Sorting a header sends the request straight to the API and only the current page reaches the grid; swap the URL and the adapter for jsonServerAdapter or offsetLimitAdapter to point at another API.
Real rows over the network - no mock, no seeded array. The grid talks to https://dummyjson.com/products through the enterprise createRestDataSource, and a *shape adapter* (dummyJsonAdapter) teaches it that API's dialect: skip/limit paging, sortBy/order sorting, rows under products with the count in total. Sort a header or page - the request goes straight to dummyjson.com and only the current page ever reaches the grid.
The same pattern points at any public API: swap the URL + adapter (jsonServerAdapter for json-server / JSONPlaceholder, or the configurable offsetLimitAdapter).
Imports, features and API used
Imports: @svgrid/grid, @svgrid/enterprise
Table features registered: rowSortingFeature
Columns: id (ID), title (Product), brand (Brand), category (Category), price (Price), rating (Rating), stock (Stock)
Frequently asked questions
What is a shape adapter?
A small object that maps the data source's generic request (page, sort) to the API's query parameters and its response to { rows, total }. dummyJsonAdapter, jsonServerAdapter and offsetLimitAdapter ship with @svgrid/enterprise; writing one for your own API is a few lines.
How are errors handled?
The data source exposes an error state that the demo renders as a message with a retry button; the loadingOverlay covers the grid while a request is in flight and emptyMessage shows when nothing loaded.
Is the REST data source in the free package?
createRestDataSource and the adapters are in @svgrid/enterprise; the grid itself and the externalSort wiring are in the MIT @svgrid/grid. The enterprise package runs without a key while you evaluate, with a small watermark.
Related documentation
Related articles
- A Svelte Data Grid with a Plain REST API - Wire SvGrid to any paginated REST endpoint - serializing sort, filter, and page state into query params, handling debounce, and cancelling stale requests before they land.
- Inside SvGrid: The Row Model and Sorting - How sorting shaped SvGrid's row-model pipeline - the decisions made early that every later feature inherited.
- Using SvGrid with TanStack Query in Svelte - Wire TanStack Query's caching and background refetch into SvGrid for a server-driven grid that pages instantly and never shows a blank screen.
Source code (497-live-rest-dummyjson.svelte)
<!-- Documented in: docs/help/server/server-row-model.md -->
<script lang="ts">
/**
* 497. Live data from a public REST API (DummyJSON)
* -------------------------------------------------
* Real rows over the network - no mock, no seeded array. The grid talks to
* https://dummyjson.com/products through the enterprise `createRestDataSource`,
* and a *shape adapter* (`dummyJsonAdapter`) teaches it that API's dialect:
* `skip`/`limit` paging, `sortBy`/`order` sorting, rows under `products` with
* the count in `total`. Sort a header or page - the request goes straight to
* dummyjson.com and only the current page ever reaches the grid.
*
* The same pattern points at any public API: swap the URL + adapter
* (`jsonServerAdapter` for json-server / JSONPlaceholder, or the configurable
* `offsetLimitAdapter`).
*/
import {
SvGrid,
createServerDataSource,
tableFeatures,
rowSortingFeature,
type GridColumns,
type ServerState,
} from '@svgrid/grid'
import { createRestDataSource, dummyJsonAdapter, setLicenseKey } from '@svgrid/enterprise'
setLicenseKey('SVENTERPRISE-DEV-LOCAL')
const features = tableFeatures({ rowSortingFeature })
type Product = {
id: number
title: string
brand: string
category: string
price: number
rating: number
stock: number
}
// A ServerDataSource over the live endpoint - the adapter supplies the
// query-building + response-parsing for DummyJSON's wire format.
const source = createRestDataSource<Product>({
url: 'https://dummyjson.com/products',
...dummyJsonAdapter<Product>(),
})
const columns: GridColumns<Product> = [
{ field: 'id', header: 'ID', width: 70, align: 'right' },
{ field: 'title', header: 'Product', width: 240 },
{ field: 'brand', header: 'Brand', width: 150 },
{ field: 'category', header: 'Category', width: 150 },
{ field: 'price', header: 'Price', width: 120, align: 'right', format: { type: 'currency', currency: 'USD' } },
{ field: 'rating', header: 'Rating', width: 110, align: 'right', format: { type: 'number', options: { minimumFractionDigits: 2, maximumFractionDigits: 2 } } },
{ field: 'stock', header: 'Stock', width: 100, align: 'right' },
]
let s = $state<ServerState<Product>>({
rows: [], total: 0, loading: false, saving: false, error: null,
pageIndex: 0, pageSize: 20, pageCount: 1, sortModel: [], filterModel: {},
})
const ctl = createServerDataSource(source, { pageSize: 20, onChange: (next) => (s = next) })
ctl.refresh()
$effect(() => () => ctl.dispose())
const rangeStart = $derived(s.total === 0 ? 0 : s.pageIndex * s.pageSize + 1)
const rangeEnd = $derived(Math.min(s.total, (s.pageIndex + 1) * s.pageSize))
</script>
<section class="wrap demo-kit">
<header class="chrome">
<span class="note">
Live products from <code>dummyjson.com</code> through <code>createRestDataSource</code>: real HTTP
requests, so open the network tab. Sorting maps to the API's <code>sortBy</code> / <code>order</code>,
paging to <code>skip</code> / <code>limit</code>; the <code>dummyJsonAdapter</code> shapes the
request and parses the response. Swap the URL and the adapter to point at any public API.
</span>
</header>
<div class="gridpane">
<SvGrid responsive={true}
columnResize
data={s.rows}
columns={columns}
features={features}
sortable
externalSort
loading={s.loading}
loadingOverlay
pageable={false}
selectionMode="none"
rowHeight={34}
containerHeight="100%"
fitColumns={true}
emptyMessage="No products loaded."
onSortingChange={(sorting) => ctl.setSort(sorting)}
/>
</div>
<footer class="foot">
<div class="actions">
<button type="button" class="btn" disabled={s.pageIndex <= 0 || s.loading} onclick={() => ctl.setPage(s.pageIndex - 1)}>Previous</button>
<button type="button" class="btn" disabled={s.pageIndex >= s.pageCount - 1 || s.loading} onclick={() => ctl.setPage(s.pageIndex + 1)}>Next</button>
</div>
<span class="stat"><span class="stat-label">Rows</span><strong>{rangeStart.toLocaleString()} - {rangeEnd.toLocaleString()}</strong> of {s.total.toLocaleString()}</span>
<span class="stat"><span class="stat-label">Page</span><strong>{s.pageIndex + 1}</strong> of {s.pageCount.toLocaleString()}</span>
{#if s.loading}<span class="stat">loading...</span>{/if}
{#if s.error}
<span class="stat err" role="alert">
Could not reach the API. {s.error instanceof Error ? s.error.message : String(s.error)}
<button type="button" class="btn" onclick={() => ctl.refresh()}>Retry</button>
</span>
{/if}
</footer>
</section>More Server-Side Row Model examples
- Server-Side Row Model: 1,000,000 rows - One grid, one rowModel prop, a million rows that stay on the server. Sort, filter, global search, grouping to any depth (Region > Country > Rep), infinite scroll or paging, inline edits applied back as transactions with the subtotal following, add and delete, select-all across rows the grid never loaded with a bulk edit by rule, failed blocks with Retry, a request log that shows every call to the columnar warehouse behind it, and a live map of the block cache per level. The row model ships in @svgrid/enterprise; the datasource contract is free.
- Server-side pivot - The pivot designer in server mode over a million rows: Rows become groupBy, Columns pivotBy, Values aggregations, and every applied layout is one request. The backend answers with one field per pivot key and aggregation and lists them in pivotResultFields; the model builds the column groups from that list. Apply / Cancel hold a slice-and-dice session to one request, a Total column group carries the row totals, and a grand total row is pinned at the bottom.
- Server grouping (row model) - Server-side grouping through one getRows contract: the request carries groupBy + groupKeys, and createServerRowModel owns the group tree - a block cache per level, lazy expand, per-group sums and a subtotal footer, race-safety - mounted through the one rowModel prop. Leaves arrive by scroll, behind a Load N more row, or paged across the whole tree, and the group panel regroups on the fly. Here a 63,000-row in-memory server behind 200ms latency; the grid holds only the groups you expand. The row model ships in @svgrid/enterprise.
- Server tree data (row model) - A file tree the grid never holds whole: expanding a folder is one getRows with the folder path as groupKeys, answered with that folder's entries one block at a time. createServerRowModel in treeData mode owns the lazy expand, a block cache per folder, open-by-default, expand and collapse all, a per-folder refresh that re-reads one folder in place, and transactions that add or delete a file without a refetch. The server generates each folder from a seeded PRNG on first request, five levels deep.
- Server transactions (live feed) - A socket-style feed of changes the server already made, applied without a refetch: a price tick patches the loaded row in place with a flash (updateRowData), a new order lands at the top of its warehouse and a shipped one leaves (applyTransactionAsync, batched every 500 ms, addressed by route). Every result carries a status the log shows: applied, cancelled under the veto hook, storeNotFound for a warehouse whose level is not cached. Refresh totals recomputes the sums a transaction leaves alone.