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:
| Domain | Methods |
|---|---|
| Records | listRecords, searchRecords, getRecord, createRecord, updateRecord, deleteRecord, bulkCreateRecords, bulkUpdateRecords, bulkDeleteRecords, restoreRecord, purgeRecord |
| Schema | listObjects, getObject, listAttributes, createObject, updateObject, addAttribute, updateAttribute, deleteAttribute |
| Search | globalSearch, globalSearchGrouped |
| Files | uploadFiles, listFiles, resolveFiles, getFile, updateFile, deleteFile, getFileUrl |
| Documents | listDocuments, getDocument, createDocument, updateDocument, attachFile, removeFile, uploadAndAttachFile, getFiles |
| Views | listViews, getView, getViewsForObject, createView, updateView, setDefaultView |
| Relations | getRelationOptions, resolveRelations, resolveRelationsBatch |
| Forms | listForms, getForm, createSubmission, saveStep, advanceStep |
| Users & permissions | getMyPermissions, getMyObjectPermissions, plus the user and role management methods |
| API keys | createApiKey, 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.