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

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>

View this example on GitHub

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.