REST & custom APIs

Not every backend is a database Studio can introspect. When you have an existing REST API - or any bespoke data layer - back the screen with a ServerDataSource. Use the built-in createRestDataSource for a conventional JSON API, or implement the four methods by hand for full control. Either way the grid, edit form, sorting, filtering, and pagination keep working unchanged.

Turnkey: createRestDataSource

Point it at a collection URL and you're done:

import { createRestDataSource } from '@svgrid/enterprise'
import { customersSchema, type Customer } from '$lib/customers'

export const customersSource = createRestDataSource<Customer>({
  url: '/api/customers',
  schema: customersSchema,          // resolves the id field for update/delete
  headers: () => ({ authorization: `Bearer ${getToken()}` }),  // optional
})

Default wire format (every part is overridable):

Call Request
read GET {url}?offset&limit&sort&search&<col>=<op>:<value>
create POST {url} with the new row as JSON
update PATCH {url}/{id} with the patch
delete DELETE {url}/{id}

sort is a comma list with - for descending (-createdAt,name). Totals come from a rowCount / total field, or a Content-Range header for array responses. When your API differs, pass buildQuery(request) to shape the read params and parse(body, response) to read rows + total out of your response:

createRestDataSource<Customer>({
  url: '/api/customers',
  schema: customersSchema,
  buildQuery: (req) => ({ page: String(req.pageIndex + 1), size: String(req.pageSize) }),
  parse: (body) => ({ rows: body.results, rowCount: body.count }),
})

Full control: implement ServerDataSource by hand

For an unusual API, supply the four methods yourself - translate the grid's request into your calls:

import type { ServerDataSource, ServerRequest } from '@svgrid/grid'

type Customer = { id: string; name: string; email: string }

export const customersSource: ServerDataSource<Customer> = {
  async getRows(req: ServerRequest) {
    const params = new URLSearchParams({
      offset: String(req.startRow),
      limit: String(req.pageSize),
    })
    if (req.filterModel.global) params.set('q', req.filterModel.global)
    const sort = req.sortModel[0]
    if (sort) params.set('sort', `${sort.desc ? '-' : ''}${sort.id}`)

    const res = await fetch(`/api/customers?${params}`)
    const body = await res.json()
    // Return the page + the TOTAL count (for the pager).
    return { rows: body.data, rowCount: body.total }
  },

  createRow: (input) =>
    fetch('/api/customers', { method: 'POST', body: JSON.stringify(input) }).then((r) => r.json()),

  updateRow: (id, patch) =>
    fetch(`/api/customers/${id}`, { method: 'PATCH', body: JSON.stringify(patch) }).then((r) => r.json()),

  deleteRow: (id) =>
    fetch(`/api/customers/${id}`, { method: 'DELETE' }).then(() => undefined),
}

Then wrap it in the controller and render the grid exactly as with any other source (see Data binding):

const controller = createServerDataSource(customersSource, {
  pageSize: 25,
  optimistic: true,
  getRowId: (r) => r.id,
  onChange: (s) => (view = s),
})

Server-side paging, sorting, and filtering are already wired - the grid emits onSortingChange / onFiltersChange / onPaginationChange and the controller turns them into new getRows requests.

The SvelteKit transport (no boilerplate)

If your API is your SvelteKit app, skip the hand-written fetch calls. The built-in transport gives you a typed client + a ready request handler over one JSON endpoint:

// src/routes/api/customers/+server.ts (server)
import { createKitHandlers } from '@svgrid/enterprise'
import { customersSchema } from '$lib/customers.schema'
import { customersBackend } from '$lib/server/customers' // any ServerDataSource
export const { POST } = createKitHandlers({ schema: customersSchema, source: customersBackend })
// client
import { createKitDataSource } from '@svgrid/enterprise'
const source = createKitDataSource({ endpoint: '/api/customers' })

createKitDataSource implements ServerDataSource for you - it posts the grid's request to the endpoint, and createKitHandlers runs it against your backend source and returns JSON.

Supabase, GraphQL, gRPC, ...

The same pattern covers any client:

Helpers for SQL-shaped backends

If your API speaks SQL under the hood, planQuery + planToSql turn the grid's request into portable, parameterized SQL you can send anywhere - see Databases and createSqlDataSource.

Common gotchas

Symptom Cause and fix
Pager stuck at page 1, or "1 to 10 of 10" when there are more getRows did not return the total rowCount. Return it from a total/rowCount field or a Content-Range header (parse reads it).
Create / edit fails, or edits hit the wrong row The source can't find the id. Pass schema (so it resolves idField), and make sure your API echoes the created row with its id.
CORS errors for a third-party API The browser blocks cross-origin calls. Proxy through your own +server.ts route (which can also hold the API key) instead of calling the third party from the client.
Filter operators ignored Only the default wire format maps operators automatically; a custom API needs buildQuery to translate req.filterModel.columns into your params.
Token expired mid-session Use the headers callback (not a static object) so a fresh token is read on every request.

See also