Skip to content
DocsAPI ReferenceJavaScript Client

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/chat when agentSlug is set
  • POST /v1/routers/:slug/chat when only routerSlug is set
  • POST /v1/chat when agentId is set (with optional routerSlug in 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