How to use the SDK
Install @stndrds/client, create one server-side client, and know what it covers before you write your first object.
@stndrds/client is one package. It carries the schema builders, defineSchemaSource, createStandards and the error classes. You declare your objects in TypeScript, sync them to a workspace with schema.sync, and read and write typed records with from. Nothing else is required: no runtime, no framework adapter.
pnpm add @stndrds/clientThe package runs on Node 18 or newer. defineSchemaSource hashes the source with node:crypto, so an edge runtime is not supported.
Create the client
import { createStandards } from "@stndrds/client";
export const standards = createStandards({
baseUrl: "https://api.standards.new/v1",
apiKey: process.env.STANDARDS_API_KEY!,
});createStandards takes four options and nothing else.
| Option | Required | Default | Effect |
|---|---|---|---|
baseUrl | yes | — | The API root, /v1 included. A dedicated instance uses its own hostname. |
apiKey | yes | — | Sent as Authorization: Bearer. schema.sync needs a key whose role grants architect:update. |
tenantId | no | none | Sent as X-Tenant-ID on every request. |
timeoutMs | no | 30_000 | Applied per request through AbortSignal.timeout. |
There is no fetch override, no custom headers option and no retry. Retries, if you need them, live in your code.
The client is server-only. When a window global exists and an apiKey is
set, createStandards throws StandardsConfigurationError before sending
anything. Keep the key in Route Handlers, Server Components, scripts and
cron jobs.
The surface
| Export | What it does |
|---|---|
createStandards(config) | Returns { schema, from }. |
standards.schema.sync(source) | GET the source's summary, PUT it when the hash or the presentation differs. Never throws for "nothing to do". |
standards.from(builder) | A typed RecordsQuery for one object: filters, sorts, fetch, single, get, create, update, delete. |
standards.from("name") | The same query, untyped, for an object your code does not declare. |
defineSchemaSource(id, { objects, views?, label?, icon? }) | Builds the objects once, hashes them, keeps the builders for inference. |
object, text, number, date, select, status, relation, document, … | The builders. listView, detailView, group and content declare views. |
RecordInput, RecordUpdate, ExtractRecord, RecordMetadata, … | The inference helpers. |
StandardsRequestError, StandardsAuthError, SchemaSourceConflictError, SchemaSourceValidationError, StandardsConfigurationError | The error classes. Builders throw ValidationError and DuplicateError. |
createTransport, createRecordsQuery, reviveRecord | The lower layer, for raw calls and tests. |
What the client does not cover
The typed client covers schema sync and records; it does not cover files, documents, global search, agents, forms and bulk routes. Which tool uploads is settled on Files and documents.
When you need one of them from Node, createTransport gives you the same authenticated fetch the client uses, without the typing:
import { createTransport } from "@stndrds/client";
const transport = createTransport({
baseUrl: "https://api.standards.new/v1",
apiKey: process.env.STANDARDS_API_KEY!,
});
const hits = await transport.get<{ data: unknown[] }>("/search", { q: "latency" });
const form = new FormData();
form.append("files", new Blob([pdfBytes], { type: "application/pdf" }), "report.pdf");
const uploaded = await transport.postMultipart("/files/upload", form);The transport exposes get, post, patch, put, delete, getText and postMultipart. It throws the same error classes as the client. Nothing on it revives dates or flattens values.
Recommended layout
Keep three files under src/standards/, as the status page cookbook does.
src/standards/
schema.ts # the builders and the schema source
client.ts # createStandards, one instance per process
sync-schema.ts # a script: standards.schema.sync(source)// src/standards/schema.ts
import { date, defineSchemaSource, number, object, relation, text } from "@stndrds/client";
export const service = object({ name: "services", label: "Service" })
.attribute(text({ name: "name", label: "Name" }).required())
.attribute(text({ name: "url", label: "URL" }).url().required())
.labelExpression("{{ name }}");
export const check = object({ name: "checks", label: "Check" })
.attribute(relation({ name: "service", label: "Service" }).to("services").required())
.attribute(number({ name: "latencyMs", label: "Latency (ms)" }))
.attribute(date({ name: "checkedAt", label: "Checked at" }).includeTime().required())
.labelExpression("{{ checkedAt }}");
export const statusSource = defineSchemaSource("status-site", {
objects: [service, check],
label: "Status page",
});// src/standards/client.ts
import { createStandards, type Standards } from "@stndrds/client";
let instance: Standards | null = null;
export function getStandards(): Standards {
if (!instance) {
instance = createStandards({
baseUrl: process.env.STANDARDS_API_URL!,
apiKey: process.env.STANDARDS_API_KEY!,
});
}
return instance;
}// src/standards/sync-schema.ts
import { getStandards } from "./client";
import { statusSource } from "./schema";
const result = await getStandards().schema.sync(statusSource);
console.log(result.applied ? `applied ${result.hash}` : "already in sync");Run the sync script from CI or by hand before the app starts. Sync is idempotent: an unchanged source costs one GET.
Testing without the network
standards.from(builder) is createRecordsQuery(transport, builder) under the hood, and the transport argument only needs get, post, put and delete. Hand it an in-memory object and every query stays typed:
import { createRecordsQuery, type Standards } from "@stndrds/client";
export const standards: Pick<Standards, "from"> = {
from: ((target) => createRecordsQuery(memoryTransport, target)) as Standards["from"],
};Type your application against Pick<Standards, "from"> and both the real client and the double satisfy it. The status page cookbook ships a working double that emulates filters, sorts and paging.
Next steps
- Objects: declare your first object and its label.
- Sync: push a source and read the result.
- Records: filters, paging, and the write verbs.
- Status page cookbook: the layout above, end to end.