Browser
Browser Extension
Section titled “Browser Extension”uql-orm/browser consumes the REST API served by the HTTP core or the Express extension, using the same query syntax you write on the server. It is optional, and it is an HTTP client rather than a driver: something on the other end still holds the connection.
import { HttpQuerier } from 'uql-orm/browser';import { User } from './shared/models/index.js';
const querier = new HttpQuerier('https://api.yourdomain.com/api');
const { data: users } = await querier.findMany(User, { $select: { email: true }, $populate: { profile: { $select: { picture: true } } }, $where: { email: { $endsWith: '@domain.com' } }, $sort: { createdAt: 'desc' }, $limit: 10,});// typed User[], each with a typed profileEntity classes are shared between backend and frontend, so the query type-checks identically on both sides.
Client API
Section titled “Client API”Every wire operation has a typed method: findMany, findManyAndCount, findOne, findOneById, count, insertOne, insertMany, saveOne (upsert via PUT), saveMany, updateOneById, updateMany, deleteOneById, deleteMany. Responses are { data, count? }.
URLs derive from the shared CRUD_ROUTES contract in uql-orm/http, and a compile-time check guarantees the client covers every operation, so the mapping cannot drift from the server.
Failed requests throw a RequestError carrying the server’s message and the numeric HTTP status, so status-driven flows work without string matching:
import { RequestError } from 'uql-orm/browser';
try { await querier.findMany(User, {});} catch (err) { if (err instanceof RequestError && err.status === 401) { location.href = '/login'; }}Options
Section titled “Options”// per instance: defaults that per-call options overrideconst querier = new HttpQuerier('/api', { headers: { Authorization: `Bearer ${session.token}` } });
// per call: abort/timeout, headers, and `silent` to skip the notification busawait querier.findMany(User, q, { signal: AbortSignal.timeout(120_000), headers: { Authorization: `Bearer ${session.token}` },});Build one scoped instance per server-side request rather than reusing a module-level one, so a token never leaks across requests.
For non-CRUD endpoints (/api/payments/checkout, …), the typed helpers get, post, put, patch, remove and query are exported too, sharing the same envelope, headers, notifications and RequestError.
HTTP QUERY transport
Section titled “HTTP QUERY transport”Opt in to send read queries in the request body instead of the URL, which sidesteps URL-length limits on large $where/$populate:
const querier = new HttpQuerier('/api', { readMethod: 'QUERY' });findOne, findMany and count then use QUERY; writes and by-id reads keep their canonical methods. The default stays GET because cross-origin QUERY needs a CORS preflight and some proxies still drop the method. The server accepts both at once, so this is a per-client switch.
Request notifications
Section titled “Request notifications”A small pub/sub bus (on) emits start, success, error and complete per request, which is enough for a global spinner in a vanilla app. Libraries that already track loading state do not need it: pass { silent: true }. See the TanStack Query recipe, where the serializable query doubles as the cache key.