JavaScript Client
Typed JS/TS client for the Struere HTTP API
The struere npm package ships a typed JavaScript client under the struere/client subpath. It wraps the Chat API and Data API with end-to-end TypeScript types, throws structured errors, and runs in browsers, Node 18+, Bun, and Deno with zero runtime dependencies.
Installation
bun add struere
import { StruereClient, StruereApiError } from 'struere/client'
The struere/client subpath has no dependency on the Node-only definition primitives (defineAgent, defineData, etc.), so it is safe to import from browser bundles.
Constructor
const struere = new StruereClient({
apiKey: process.env.STRUERE_API_KEY!,
})
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
— | API key for the target environment. Required |
baseUrl |
string |
'https://api.struere.dev' |
Override the API base URL |
fetch |
typeof globalThis.fetch |
globalThis.fetch |
Custom fetch implementation for tests or runtimes without a global fetch |
The client throws synchronously on construction if apiKey is missing or if no fetch is available.
Works In
- Browsers — CORS-enabled (
Access-Control-Allow-Origin: *) on Chat and Data endpoints, no proxy required - Node 18+ — uses the global
fetch - Bun — uses the global
fetch - Deno — uses the global
fetch
Chat
const reply = await struere.chat({
agentSlug: 'support',
message: 'Hi!',
})
console.log(reply.threadId, reply.message, reply.usage)
client.chat(request)
interface ChatRequest {
agentSlug?: string
agentId?: string
routerSlug?: string
message: string
threadId?: string
externalThreadId?: string
threadContext?: { params?: Record<string, unknown> }
}
Exactly one of agentSlug, agentId, or routerSlug is required. The client routes to:
POST /v1/agents/:slug/chatwhenagentSlugis setPOST /v1/routers/:slug/chatwhen onlyrouterSlugis setPOST /v1/chatwhenagentIdis set (with optionalrouterSlugin the body)
interface ChatResponse {
threadId: string
message: string
assistantMessageId?: string
usage: {
inputTokens: number
outputTokens: number
totalTokens: number
reasoningTokens?: number
}
_executionMeta?: {
iterationCount: number
model: string
durationMs: number
toolCallSummary: Array<{ name: string; durationMs: number; status: string; errorType?: string; errorMessage?: string }>
errorCount: number
permissionDenialCount: number
}
_transferred?: { targetAgentSlug: string; targetAgent: string }
}
Pass threadId to continue an existing conversation, or externalThreadId to deduplicate by an upstream identifier.
Data
The client.data namespace exposes typed CRUD and query operations against the Data API. All methods return promises and throw StruereApiError on non-2xx responses.
client.data.entityTypes()
Lists all entity types in the current environment.
const { data } = await struere.data.entityTypes()
data.forEach((t) => console.log(t.slug, t.name))
client.data.list(type, options?)
Paginated list of entities for a given type.
const players = await struere.data.list<{ name: string }>('player', { limit: 50 })
console.log(players.data, players.cursor, players.hasMore)
| Option | Type | Description |
|---|---|---|
limit |
number |
Page size |
cursor |
string |
Pagination cursor returned by a previous call |
status |
string |
Filter by entity status |
client.data.get(type, id)
Fetch a single entity by ID.
const player = await struere.data.get<{ name: string }>('player', 'e57abc123')
client.data.create(type, data, options?)
Create a new entity.
const created = await struere.data.create('player', { name: 'Mia', team: 'red' })
client.data.update(type, id, patch, options?)
Patch an existing entity. Only the fields you pass are updated.
const updated = await struere.data.update('player', 'e57abc123', { team: 'blue' })
client.data.delete(type, id)
Delete an entity by ID.
await struere.data.delete('player', 'e57abc123')
client.data.query(type, options?)
Filtered query with pagination.
const matches = await struere.data.query('player', {
filters: { team: { $eq: 'red' }, score: { $gt: 100 } },
limit: 25,
})
| Option | Type | Description |
|---|---|---|
filters |
Record<string, FilterValue> |
Field filters (see operators below) |
status |
string |
Filter by entity status |
limit |
number |
Page size |
cursor |
string |
Pagination cursor |
Filter Operators
A FilterValue is either a literal value (implicit $eq) or one of:
| Operator | Type | Description |
|---|---|---|
$eq |
unknown |
Field equals value |
$neq |
unknown |
Field does not equal value |
$in |
unknown[] |
Field value is one of the array members |
$contains |
unknown |
Field contains substring or array element |
$gt |
unknown |
Greater than |
$gte |
unknown |
Greater than or equal |
$lt |
unknown |
Less than |
$lte |
unknown |
Less than or equal |
$exists |
boolean |
Field is present (true) or absent (false) |
const active = await struere.data.query('player', {
filters: {
team: { $in: ['red', 'blue'] },
nickname: { $contains: 'fox' },
retiredAt: { $exists: false },
},
})
client.data.search(type, options)
Full-text search across the entity type's searchFields.
const results = await struere.data.search<{ name: string }>('player', {
query: 'mia',
limit: 10,
})
Errors
All client methods throw StruereApiError on non-2xx HTTP responses.
import { StruereApiError } from 'struere/client'
try {
await struere.data.get('player', 'missing')
} catch (err) {
if (err instanceof StruereApiError) {
console.error(err.status, err.message, err.code, err.requestId)
} else {
throw err
}
}
| Field | Type | Description |
|---|---|---|
status |
number |
HTTP status code |
message |
string |
Error message from the server, or HTTP <status> if none |
code |
string? |
Machine-readable error code, when provided |
requestId |
string? |
Request identifier for correlating with server logs |
details |
string? |
Additional human-readable detail |
body |
unknown? |
Parsed response body |
Filter validation errors
The Data API rejects filters that target unsupported top-level fields. Both errors return 400 Bad Request and surface through StruereApiError.
Unsupported top-level filter field. Top-level filters must be on indexed columns. Domain fields stored in the JSON payload must be referenced as data.<fieldName>.
try {
await struere.data.query('session', {
filters: { teacherId: 'usr_123' },
})
} catch (err) {
if (err instanceof StruereApiError) {
console.error(err.status, err.message)
}
}
{
"error": "Unsupported filter field 'teacherId'. Queryable top-level fields: matchId. For fields stored in the entity payload, use 'data.teacherId'."
}
Top-level status filter rejected. Top-level status is the entity lifecycle column, not your domain status. The error points to data.status.
try {
await struere.data.query('session', {
filters: { status: 'scheduled' },
})
} catch (err) {
if (err instanceof StruereApiError) {
console.error(err.status, err.message)
}
}
{
"error": "Top-level 'status' filter is not supported (it targets the entity lifecycle column). Use filter: { 'data.status': 'scheduled' } for your domain status field."
}
The fix in both cases is to prefix the field with data.:
await struere.data.query('session', {
filters: { 'data.status': 'scheduled', 'data.teacherId': 'usr_123' },
})
Browser Example: Vite + React
A minimal Vite component that lists entities and renders them. Requires a development API key created via bunx struere keys create --env development and stored in VITE_STRUERE_API_KEY.
import { useEffect, useState } from 'react'
import { StruereClient, StruereApiError } from 'struere/client'
const struere = new StruereClient({
apiKey: import.meta.env.VITE_STRUERE_API_KEY,
})
interface Player {
name: string
team: string
}
export function PlayerList() {
const [players, setPlayers] = useState<Player[]>([])
const [error, setError] = useState<string | null>(null)
useEffect(() => {
struere.data
.list<Player>('player', { limit: 25 })
.then((res) => setPlayers(res.data.map((e) => e.data)))
.catch((err) => {
if (err instanceof StruereApiError) {
setError(`${err.status}: ${err.message}`)
} else {
setError(String(err))
}
})
}, [])
if (error) return <p>Error: {error}</p>
return (
<ul>
{players.map((p, i) => (
<li key={i}>{p.name} ({p.team})</li>
))}
</ul>
)
}
For chat from a browser, swap data.list for chat():
const reply = await struere.chat({
agentSlug: 'support',
message: 'Hi!',
})
See Also
- API Overview — Authentication, base URL, CORS
- Chat API — Raw HTTP request/response for chat
- Data API — Raw HTTP request/response for entity CRUD
- struere keys — Create the API key the client uses