standards
SDK

SchemaClient

The imperative, typed HTTP client behind the React hooks — for scripts, server code, and anywhere outside a component.

Every React hook delegates to SchemaClient, a typed HTTP client from @stndrds/react. Use it directly when a hook doesn't fit: scripts, Next.js route handlers, tests, or logic that runs outside the React tree. (Inside the standards NestJS backend itself, use Backend data access instead — no HTTP round-trip.)

Getting a client

Inside React, grab the one your provider already configured:

import { useSchemaClient } from "@stndrds/react";

const client = useSchemaClient();

Outside React, construct one. The constructor takes the same SchemaClientConfig as the provider. In a script, the simplest way to authenticate is an API key in headers:

import { SchemaClient } from "@stndrds/react";

const client = new SchemaClient({
  baseUrl: "https://api.standards.new",
  apiPrefix: "/v1",
  headers: { Authorization: `Bearer ${process.env.STNDRDS_API_KEY}` },
});

Working with records

Like the hooks, record methods accept a type parameter that types the record's fields end to end:

import type { ExtractRecord } from "@stndrds/schema";

type Contact = ExtractRecord<typeof CONTACT>;

const { data, page } = await client.listRecords<Contact>("contacts", {
  limit: 50,
  filters: {
    combinator: "and",
    rules: [{ attribute: "status", operator: "is", value: "active" }],
  },
});

const created = await client.createRecord("contacts", {
  firstName: "Ada",
  lastName: "Lovelace",
});

await client.updateRecord("contacts", created.id, { email: "[email protected]" });
await client.deleteRecord("contacts", created.id);

The client normalizes every list-style response to { data, page } — including plain lists, which the raw API returns as { records, total }. One envelope, no special cases.

One asymmetry to know: createRecord takes the values directly, while the useCreateRecord hook and the REST endpoint wrap them in data.

Filtering on dates works here too. The client sends no timezone of its own, so @today resolves in UTC — set one in the constructor config or with client.configure({ headers: { "x-timezone": "Europe/Paris" } }) when the day boundary matters.

Bulk methods follow the API's bulk semantics: bulkCreateRecords, bulkUpdateRecords, and bulkDeleteRecords all report partial success ({ created, errors } / { updated, errors } / { deleted, errors }) — an invalid item is reported in errors while the rest of the batch proceeds, not a fail-fast rejection. listDeletedRecords, restoreRecord, and purgeRecord cover the trash lifecycle.

The method surface

The client is flat — every domain's methods live directly on the instance. Representative methods per domain:

DomainMethods
RecordslistRecords, searchRecords, getRecord, createRecord, updateRecord, deleteRecord, bulkCreateRecords, bulkUpdateRecords, bulkDeleteRecords, restoreRecord, purgeRecord
SchemalistObjects, getObject, listAttributes, createObject, updateObject, addAttribute, updateAttribute, deleteAttribute
SearchglobalSearch, globalSearchGrouped
FilesuploadFiles, listFiles, resolveFiles, getFile, updateFile, deleteFile, getFileUrl
DocumentslistDocuments, getDocument, createDocument, updateDocument, attachFile, removeFile, uploadAndAttachFile, getFiles
ViewslistViews, getView, getViewsForObject, createView, updateView, setDefaultView
RelationsgetRelationOptions, resolveRelations, resolveRelationsBatch
FormslistForms, getForm, createSubmission, saveStep, advanceStep
Users & permissionsgetMyPermissions, getMyObjectPermissions, plus the user and role management methods
API keyscreateApiKey, listApiKeys, revokeApiKey

addAttribute and updateAttribute resolve to the saved attribute with an optional warnings: string[]. A save that produces warnings still succeeded — a formula may reference an attribute that does not exist yet, which is allowed so attributes can be created in any order — but the formula will not compute until the reference resolves. Show warnings when it is present: nothing else distinguishes a forward reference from a typo.

const attribute = await client.addAttribute(objectId, {
  name: "total",
  label: "Total",
  type: "formula",
  formula: "price * quantitiy",
});

if (attribute.warnings?.length) {
  // ['Unknown attribute "quantitiy"']
}

Almost every hook has a same-named client counterpart — if you know the hook, you know the method.

Error handling

Failed requests throw SchemaApiError, which carries the HTTP status and the message parsed from the error envelope:

import { SchemaApiError } from "@stndrds/react";

try {
  await client.getRecord("contacts", id);
} catch (error) {
  if (error instanceof SchemaApiError && error.status === 404) {
    return null; // record was deleted
  }
  throw error;
}

A status of 0 means the request never got an HTTP response — a timeout, an abort, or a network failure. The client retries failed network requests automatically (with backoff) before giving up.

Reconfiguring at runtime

  • client.configure(partialConfig) — patch the config in place (e.g. swap headers after a token refresh).
  • client.setTenantId(id) — switch the workspace for subsequent requests in multi-tenant setups.
  • client.withScope({ pathPrefix, headers? }) — a scoped copy of the client whose calls go through a different URL prefix, used by features whose endpoints live under one (such as workflows). The original client is untouched.

When to prefer the hooks

Inside React components, the hooks add caching, optimistic updates, and invalidation for free. Reach for the client when you're outside the component tree, or when you don't want the query cache involved. If you mutate through the client while hooks display the same data, invalidate the affected queries with the exported query-key factories.