Defjs

Configure how your application calls imported SDK operations, independently of their API definitions.

Client

A Client is the consuming application's execution configuration. createClient(...) returns a DerivableClient. Calling the factory with request options returns a Client value, and a Client value is what operations accept. It is not callable. Import operations from your SDK, choose the service address, credentials, interceptors, and transport, then call an operation with the client. getUser(client({ signal }), { id: 1 }) starts a call that declares input. health(client) starts one that does not. When the whole input struct is optional, both search(client) and search(client, input) are valid. There is no third options argument. Calling the operation starts it. await only waits for the error-first tuple.

The SDK's definitions do not bind this instance. A browser, a server request, and a local test can use the same exported operation with different clients. All still follow the same Defjs contract and compatible Core versions. Configure each client's service address for the operations you call through it.

The same client value can be passed to many operations. Applications choose caching and retry policies and own the work they start. HTTP is not retried automatically. Stream reconnection is opt-in.

The factory is a function and is also a Client with no request options. Do not pass the factory to a React state setter: setState(factory) would call it as an updater. Do not use it as a Vue watch or computed source: those call a function. A derived client value is a plain object.

Basic Setup

import { createClient, withEndpoint } from "@defjs/core";

const client = createClient(withEndpoint("https://api.example.com"));

createClient(...) returns one callable factory that is also a Client. It is not a set of transport overloads.

Compose options

Options apply left to right. Setters replace; withInterceptors(...items) appends.

import {
  createClient,
  createHttpInterceptor,
  withCredentials,
  withEndpoint,
  withInterceptors,
} from "@defjs/core";

const audit = createHttpInterceptor(async (request, next) => {
  const started = performance.now();
  const response = await next(request);
  console.info(
    request.operation ?? request.method,
    response.status,
    Math.round(performance.now() - started),
  );
  return response;
});

const client = createClient(
  withEndpoint("https://api.example.com"),
  withInterceptors(audit),
  withCredentials(true),
);
void client;

Mixed interceptors are filtered by transport when the operation runs; relative order among the selected kind stays.

Derive a client

client(options?) returns a Client value for one call. The seven known fields are copied from the argument object when client(options) is called, a top-level snapshot into a fresh options object. Unknown fields are ignored and fields whose value is undefined are omitted. There is no parent overlay: client(), client(undefined), and client({}) return a client with no request options. Nested heartbeat, protocols, and reconnect values are kept by reference, not cloned. WebSocket reads them when the operation runs (it copies heartbeat fields and the protocols list at that time), so mutating those objects after deriving can still affect a later call. Applications should treat them as immutable after passing them. Deriving performs no I/O. A getter that throws escapes client(options) and leaves the factory usable. The client value is not callable; create one per call.

signal is not combined. Reusing a client value that carries signal shares that signal between calls, so create a client value per call. Combine signals with AbortSignal.any in application code.

import { createClient, defineRequest, struct, withEndpoint } from "@defjs/core";

const client = createClient(withEndpoint("https://api.example.com"));
const getUser = defineRequest({
  method: "GET",
  path: "/users/:id",
  input: struct.object({ id: struct.number() }),
  builder: (input) => ({ path: input }),
  output: struct.object({ id: struct.number(), name: struct.string() }),
});

async function main(signal: AbortSignal) {
  const requestClient = client({
    signal,
    onDownloadProgress(event) {
      console.log(event.loaded);
    },
  });
  const [error, user] = await getUser(requestClient, { id: 1 });
  if (error) {
    console.error(error.code);
    return;
  }
  console.log(user.name);
}

void main;

The base configuration from createClient is shared by every client value and is not frozen at runtime. It is fixed by convention: nothing in the library changes it, and applications should not either. The RequestOptions on a client are that top-level snapshot. Application code passes the client to an operation. It does not read that internal state.

Request options

RequestOptions has seven flat fields. Each protocol reads only the fields it knows and ignores the rest. Passing heartbeat to an HTTP operation is ignored, not a fault. SSE reads only signal from request options. SSE reconnect comes from client configuration (withSSEReconnect), so client({ reconnect }) affects WebSocket only. There is no compile-time protocol filter.

FieldUsed byRule
signalHTTP, SSE, WebSocketAborts the call. A deadline is AbortSignal.timeout(ms) (NET_TIMEOUT). A manual abort is NET_ABORTED.
onUploadProgressHTTPIgnored by SSE and WebSocket.
onDownloadProgressHTTPIgnored by SSE and WebSocket.
beforeConnectWebSocketReplaces the client setting for that call.
protocolsWebSocketReplaces the client list. It is not appended.
heartbeatWebSocketReplaced whole. timeoutMs is the heartbeat acknowledgement wait, not a request deadline.
reconnectWebSocketReplaced whole.

There is no timeout field, no abort field, and no client-level default deadline. For SSE and WebSocket, signal covers the whole connection, including after it is open. There is no startup-only deadline. An application that wants one creates its own AbortController, aborts it with new DOMException("...", "TimeoutError") from a timer, and clears the timer after the await returns. See SSE.

HttpExecuteOptions is the HTTP slice: signal plus the progress callbacks. EventStreamExecuteOptions is { signal? }. WebSocketExecuteOptions is the WebSocket slice plus signal.

Results by transport

  • HTTP → [error, data, response]
  • SSE → [error, stream, open]. open is the returned connection's first successful handshake snapshot for native Defjs handles or wrappers forwarding the same live open object; custom handles or wrappers overriding open use their current public open even after reconnect, while stream.open stays live (see SSE).
  • WebSocket → [error, session, connection]

Return before using the success value when error is set.

You own cleanup: abort HTTP, close SSE + await stream.closed, close WebSocket + await session.closed.

Inject a test transport

import {
  createClient,
  defineRequest,
  struct,
  withEndpoint,
  withHTTPHandle,
} from "@defjs/core";

const getUser = defineRequest({
  method: "GET",
  path: "/users/:id",
  input: struct.object({ id: struct.number() }),
  builder: (input) => ({ path: input }),
  output: struct.object({ id: struct.number(), name: struct.string() }),
});

const client = createClient(
  withEndpoint("https://fixture.invalid"),
  withHTTPHandle(async () => Response.json({ id: 7, name: "Ada" })),
);
async function main() {
  const [error, user] = await getUser(client, { id: 7 });
  if (error) {
    console.error(error.code);
    return;
  }
  console.log(user.name);
}

void main();

Scope on server vs browser

On a server, create the client inside the request boundary when options or interceptor closures capture auth, cookies, users, or tenants. Client identity isn’t a security boundary by itself.

Migration

OldNew
client.execute(getUser(input), options)getUser(client(options), input)
execute(cmd, { timeout: 5000 })client({ signal: AbortSignal.timeout(5000) })
execute(cmd, { signal })client({ signal })
client({ signal })({ onDownloadProgress })client({ signal, onDownloadProgress })
createClient(withTimeout(5000), ...)pass signal at the call, or wrap an application function

Reference

HelperEffect
withEndpoint(url)Absolute base endpoint for all transports
withHeaders(HeadersInit)Static default headers for HTTP/SSE; builder headers override
withHTTPHandle(fetch)Replace Fetch for HTTP
withSSEHandle(fetch)Replace Fetch for SSE
withWebSocketHandle(WebSocket)Replace WebSocket constructor
withInterceptors(...items)Append mixed interceptors
withQuerySerializer(fn)Encode an already-aliased query object into a query string
withCredentials(boolean)Fetch credentials: 'include' for HTTP/SSE when true
withXSRF(options?)HTTP XSRF cookie → header
withSSEReconnect / withSSEOnInvalidEventSSE knobs
withWebSocketReconnect / withWebSocketHeartbeat / withWebSocketProtocols / withWebSocketBeforeConnectWebSocket knobs
Edit this page on GitHub
Released under the MIT License. · Copyright © 2026 MunMunMiao

On this page