@brand-map/ts-client
v0.0.10-alpha.34
Published
Brand-Map TypeScript SDK client.
Readme
TypeScript SDK Client
TypeScript client for the Brand-Map HTTP API.
The SDK mirrors the HTTP API as a typed resource tree. Published admin routes appear under sdk.admin; published storefront routes appear under sdk.store when they are included in the SDK build.
Install
npm:
npm install @brand-map/ts-clientYarn:
yarn add @brand-map/ts-clientpnpm:
pnpm add @brand-map/ts-clientBun:
bun add @brand-map/ts-clientThis build is version 0.0.10-alpha.33.
Import and create a client
import { BrandMapClient } from "@brand-map/ts-client"
const sdk = new BrandMapClient({
baseUrl: "https://brand-map.site/api",
auth: {
tokenStore: createMemoryTokenStore(),
},
timeoutMs: 15_000,
})
await sdk.auth.login({
email: "[email protected]",
password: "password1234",
})Client configuration
baseUrlis required. It is the API origin without a trailing path requirement.auth.tokenStorestores Better Auth bearer session tokens. The generated default is in-memory only.auth.clearTokenOnUnauthorizedclears the token store after a 401 response. It defaults totrue.auth.credentialssets default fetch credentials for cookie-backed Better Auth sessions.defaultHeadersare sent with every request.getHeadersruns before every request and can provide caller-managed auth headers.- Browser apps normally omit
fetch. The SDK callsglobalThis.fetchsafely by default, and custom fetch functions are invoked with the correct global binding. fetchis mainly for tests or non-standard runtimes that need to inject a custom implementation.timeoutMssets a default request timeout. A method call can override it through method options.
For persistent tokens, provide your own BrandMapTokenStore from application code:
const tokenStore = {
async get() {
return await secureStore.get("brand-map-token")
},
async set(token: string) {
await secureStore.set("brand-map-token", token)
},
async clear() {
await secureStore.delete("brand-map-token")
},
}
const sdk = new BrandMapClient({
baseUrl: "https://brand-map.site/api",
auth: { tokenStore },
})Resource tree
Resources are grouped by API scope and resource name:
// Storefront API
await sdk.store.collection.list()
await sdk.store.collection.get({ id: "collection_123" })Use editor autocomplete on sdk.admin, sdk.platform, and sdk.store to discover available resources and methods. Method names are the public SDK surface; this README intentionally does not duplicate the full route list.
Method argument order
SDK methods preserve the HTTP route shape but use typed positional arguments:
- path params first
- request body second
- query object third, or earlier when there are no params/body
- method options last
Examples:
// list route: query, options
await sdk.store.collection.list({ pagination: { take: 20 } })
// detail route: params, query, options
await sdk.store.collection.get({ id: "collection_123" }, { fields: ["id", "title"] })Querying lists
List methods accept the same query model as the HTTP API: fields, populate, filters, sort, and pagination when supported by that route.
const collections = await sdk.store.collection.list({
fields: ["id", "title", "createdAt"],
filters: {
isActive: { $eq: true },
},
sort: ["createdAt:desc"],
pagination: {
take: 20,
skip: 0,
},
})
assertSuccess(collections)
collections.data
collections.paginationPagination lives on the response envelope, not inside data. Offset pagination uses { take, skip }; page pagination uses { page, pageSize }.
assertSuccess(collections)
if (collections.pagination) {
console.log(collections.pagination.total)
}The SDK serializes query objects to the API bracket notation, so callers should pass normal JavaScript objects instead of pre-encoded query strings.
Selecting fields
For operations that support fields, inline field arrays narrow the response type without as const:
const res = await sdk.store.collection.list({
fields: ["createdAt", "description", "title"],
})
assertSuccess(res)
res.data.at(0)?.createdAt
// string | undefined
res.data.at(0)?.slug
// TypeScript error: slug was not selectedIf you store a query in a widened query type, TypeScript cannot recover literal field names and the response falls back to the broad DTO-compatible shape.
Populating relations
Routes that expose relation metadata accept populate. Relations are included only when they are explicitly populated. Relation nodes must describe what they need; empty nodes are rejected before the request is sent.
// Collection routes currently expose only scalar selection in the published SDK.
const res = await sdk.store.collection.list({
fields: ["id", "title"],
})
assertSuccess(res)
res.data.at(0)?.title
// string | undefinedWhen a route supports populate, nested selection follows the same relation-node shape and narrows nested response data.
Invalid empty selection shapes throw in the SDK client before fetch is called:
await sdk.store.collection.list({ fields: [] })
// Error: query.fields must not be an empty array
await sdk.store.collection.list({ sort: [] })
// Error: query.sort must not be an empty array
await sdk.store.collection.list({ pagination: {} })
// Error: query.pagination must not be an empty object
await sdk.store.collection.list({ take: 20 })
// Error: query.take is not supported. Use query.pagination.take.Autocomplete is available throughout query objects: top-level fields, supported populate keys, relation node options, nested fields, and nested populate keys.
Method options
The final argument of every SDK method is BrandMapMethodOptions:
await sdk.store.collection.get(
{ id: "collection_123" },
{ fields: ["id", "title"] },
{
headers: {
"X-Request-Source": "collection-page",
},
timeoutMs: 30_000,
},
)Use idempotencyKey for routes that support idempotency. For routes that do not support it, the SDK does not send the Idempotency-Key header.
Response envelope
Every successful request resolves to the Brand-Map response envelope:
type BrandMapErrorStatusMap = {
400: "badRequest"
401: "unauthorized"
403: "forbidden"
404: "notFound"
409: "conflict"
422: "unprocessableEntity"
429: "tooManyRequests"
500: "internalServerError"
504: "gatewayTimeout"
}
type BrandMapErrorStatus = keyof BrandMapErrorStatusMap
type BrandMapErrorStatusKey = BrandMapErrorStatusMap[BrandMapErrorStatus]
type BrandMapErrorPayload<Status extends BrandMapErrorStatus = BrandMapErrorStatus> = {
[CurrentStatus in Status]: {
cause?: unknown
code?: string
message: string
name: string
status: CurrentStatus
statusKey: BrandMapErrorStatusMap[CurrentStatus]
type?: string
}
}[Status]
type BrandMapSuccessResponse<Data, Paginated extends boolean = false> = {
data: Data
error: null
meta: Record<string, JsonValue> | null
pagination: Paginated extends true ? BrandMapPagination : null
}
type BrandMapErrorResponse<ErrorPayload extends BrandMapErrorPayload = BrandMapErrorPayload> = {
data: null
error: ErrorPayload
meta: Record<string, JsonValue> | null
pagination: null
}
type BrandMapResponse<
Data,
Paginated extends boolean = false,
ErrorPayload extends BrandMapErrorPayload = BrandMapErrorPayload,
> = BrandMapSuccessResponse<Data, Paginated> | BrandMapErrorResponse<ErrorPayload>
function isSuccess<Data, Paginated extends boolean, ErrorPayload extends BrandMapErrorPayload = BrandMapErrorPayload>(
response: BrandMapResponse<Data, Paginated, ErrorPayload>,
): response is BrandMapSuccessResponse<Data, Paginated>
function isError<Data, Paginated extends boolean, ErrorPayload extends BrandMapErrorPayload = BrandMapErrorPayload>(
response: BrandMapResponse<Data, Paginated, ErrorPayload>,
): response is BrandMapErrorResponse<ErrorPayload>
function assertSuccess<Data, Paginated extends boolean, ErrorPayload extends BrandMapErrorPayload = BrandMapErrorPayload>(
response: BrandMapResponse<Data, Paginated, ErrorPayload>,
): asserts response is BrandMapSuccessResponse<Data, Paginated>
async function unwrap<Data, Paginated extends boolean, ErrorPayload extends BrandMapErrorPayload = BrandMapErrorPayload>(
responsePromise: PromiseLike<BrandMapResponse<Data, Paginated, ErrorPayload>>,
): Promise<Data>Use isSuccess, isError, or assertSuccess to narrow the union. Use unwrap when a caller only needs success data. For list routes, success data is an array. For item routes, success data is a single object.
import { assertSuccess, isError, unwrap } from "@brand-map/ts-client"
const response = await sdk.store.collection.list({ pagination: { take: 10 } })
if (isError(response)) {
console.error(response.error)
return
}
response.data
response.pagination
assertSuccess(response)
response.data
const collections = await unwrap(sdk.store.collection.list({ fields: ["id", "title"] }))
collections[0]?.idErrors
Non-2xx responses throw BrandMapFetchError. The error contains the original Response and parsed response payload.
import { BrandMapFetchError } from "@brand-map/ts-client"
try {
await sdk.store.collection.get({ id: "missing" })
} catch (error) {
if (error instanceof BrandMapFetchError) {
console.error(error.response.status)
console.error(error.payload)
}
}Browser usage
Modern bundlers should import the ESM entry automatically:
import { BrandMapClient } from "@brand-map/ts-client"The package also publishes a UMD bundle for script-tag usage. It exposes BrandMapSdk on globalThis:
<script src="/path/to/ts-client.umd.js"></script>
<script>
const sdk = new BrandMapSdk.BrandMapClient({
baseUrl: "https://brand-map.site/api",
})
</script>Testing
Pass a custom fetch implementation to test code without hitting the network:
const sdk = new BrandMapClient({
baseUrl: "https://api.test",
fetch: async () =>
new Response(JSON.stringify({ data: [], error: null, meta: null, pagination: null }), {
headers: { "Content-Type": "application/json" },
status: 200,
}),
})Build and publish
The workspace package is private and source-oriented. The npm package is the dist directory.
cd typescript
bun run build
cd dist
npm publishDo not publish typescript directly; publish typescript/dist.
Regenerating
After changing API metadata or the SDK renderer, refresh the package from the server repo root:
bun run script gen sdk-client --module all --force