@defjs/core
Design reusable HTTP, SSE, and WebSocket APIs as data, then execute them with an application-owned client.
def means define. An SDK author exports operation definitions and data contracts. The consuming application supplies the endpoint, credentials, interceptors, and transport. Importing a definition starts no request and binds no client.
Install
bun add @defjs/core
The package is ESM and declares no runtime dependencies. Declarations target ES2022 with ESNext.Disposable, DOM, and DOM.Iterable; those entries supply types only, not polyfills. Defjs is pre-1.0, so use the documentation for the exact version you installed.
Quick start
// users-sdk.ts — SDK definition
import { defineRequest, struct } from "@defjs/core";
export 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() }),
error: struct.object({ message: struct.string() }),
});
// app.ts — application-owned client
import { createClient, withEndpoint } from "@defjs/core";
import { getUser } from "./users-sdk.ts";
const client = createClient(withEndpoint("https://api.example.com"));
async function main() {
const [error, user] = await getUser(client, { id: 1 });
if (error) {
console.error(String(error), error.code);
return;
}
console.log(user.name);
}
void main();
Calling getUser(client, { id: 1 }) validates, builds, and sends the request. client({ signal }) returns a Client value for one call and does not perform I/O. Return before reading the success value when error is set. Once published, import getUser from your SDK package instead of the local file — see Build and publish an SDK.
Rules that matter
- Calling an operation with a client performs I/O. A definition does not capture a client. There is no global client.
- Per-call options are the seven
RequestOptionsfields:signal,onUploadProgress,onDownloadProgress,beforeConnect,protocols,heartbeat, andreconnect.client(options)returns a non-callable client value that copies those fields into a fresh object; fields set toundefinedare omitted. Nestedheartbeat,protocols, andreconnectvalues are kept by reference, so treat them as immutable after passing them. PassAbortSignal.timeout(ms)assignalwhen a call needs a deadline. That reason isNET_TIMEOUT; a manual abort isNET_ABORTED. A getter that throws does so fromclient(options), not as a fault. SSE and WebSocket have no startup-only deadline: the signal aborts the whole connection. inputdeclares parameters; a synchronous, purebuilderplaces them onpath,query,headers, andbody. Every definition withinputrequires an explicitbuilder.- Structs validate at runtime.
outputis the 2xx body,errorevery other status; omit a side and that body is never read. - Failures are returned as
Faultinstances in an error-first tuple —HTTP_STATUS,REQ_*,NET_*,RES_*,EXT_*,CAP_*,ENV_UNSUPPORTED.String(error)gives the message;error.codeand the cause chain stay structured for logs. - A timeout or cancellation does not prove the server missed a write. Keep an application idempotency contract before replaying writes.
- Returned SSE and WebSocket handles are
AsyncDisposable; WebSocketclosedis the logical terminal result. - Inject a Fetch-compatible function with
withHTTPHandle(...)to test a client without a server — see Test with a handle.
Documentation
License
MIT.