rexi.js is an experimental, minimalist fetch wrapper with HTTP-verb shortcuts, form serialization, chainable body parsers, and throw-on-error semantics. It's the JSON-side companion for the fixi.js family: for the times you do need to talk to an API from client code.
Part of the fixi project.
Here is an example:
<script src="rexi.js"></script>
<script>
let me = await get("/api/me").json()
await post("/api/login", document.forms.login) // form element
await post("/api/users", {name: "Ada"}) // JSON body
await get("/search", {q: "hi"}, {include: "#filters"}) // query string + extra fields
</script>Six verb helpers (get, head, post, put, patch, del) are attached to
globalThis for zero-ceremony use. Each returns a decorated Promise<Response> with
.json(), .text(), .blob(), .html(), .raw(), and .abort() sugar so the common
case is one await.
rexi is deliberately tiny: no interceptor pipeline, no retry logic, no automatic base URL, no upload progress, no caching layer, no built-in download helper. If your app outgrows it, reach for ky or wretch.
Drop rexi.js into a script tag:
<script src="rexi.js"></script>Or install via npm:
npm install rexi-js
All six verbs share the same shape:
get|head|post|put|patch|del(url, body?, opts?)
del is used instead of delete because bare delete(x) is a JavaScript syntax trap.
Both rexi.del and window.del are exposed.
The second positional arg is a logical "input". Its type decides the wire format:
| Input | Sent as |
|---|---|
FormData |
as-is |
HTMLFormElement |
new FormData(el) |
| single named input element | FormData with one [name, value] entry |
iterable of elements (e.g. moxi q(...)) |
FormData collecting each element's [name,value] |
| plain object | JSON.stringify, Content-Type: application/json |
string / Blob / URLSearchParams / ArrayBuffer |
passed straight to fetch |
null / undefined |
no body |
By default GET / HEAD / DEL URL-encode the body into the query string (with
repeating keys for array values), and POST / PUT / PATCH send it in the request
body. Override with opts.send: "query" | "body".
{
include: selector | Element | iterable | Array<any of those>, // merge extra form fields
send: "query" | "body", // override method default
timeout: ms, // abort after N ms
signal: AbortSignal, // external cancel, chains in
headers: {...}, // merged with auto Content-Type
... // passed through to fetch()
}include with a JSON body promotes the request to form mode (the JSON object is
flattened into FormData entries).
let p = get(url, body, opts)
await p.json() // parsed JSON
await p.text() // string
await p.blob() // Blob
await p.html() // DocumentFragment (parsed via <template>)
await p.raw() // the raw Response
p.abort() // cancel the underlying fetchNon-2xx responses throw an Error carrying .status and .response (the raw
Response, for bodies/headers).
rexi dispatches two CustomEvents on document:
rexi:before- cancelable;detail.cfg = {url, init}can be mutated (e.g. to inject anAuthorizationheader).preventDefault()aborts the request withAbortError.rexi:after- fires for every completed fetch (including non-2xx, before rexi throws);detail = {cfg, response}.
document.addEventListener("rexi:before", (e) => {
e.detail.cfg.init.headers.Authorization = `Bearer ${token}`
})Zero-Clause BSD
=============
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.