Ajax
Ajax is Qite's small HTTP client built on top of
fetch(). It exists for the
common things you keep rewriting in every app:
- consistent request/response parsing
- request/response debug logging
- defaults and per-request overrides
- CSRF integration for common backends
- handler tables for success/error status codes
- predictable throw rules
The API is intentionally small. You usually call
Ajax.get(...)
or
Ajax.post(...), then
await .ready()
Why the name Ajax?
It's intentional and it doesn't try to hide what it is. Before
fetch,
before "data layers", before client abstractions that pretend HTTP does not
exist, there was Ajax: a simple, explicit way to send a request and handle a
response without reloading the page. Qite does not wrap networking in a new
philosophy or invent a magical transport concept. It embraces the boring,
honest reality: you are making an HTTP request. The name "Ajax" is a reminder
that this layer should stay small, transparent, and unsurprising.
Quick start
A request returns an Ajax instance. To await the request and get the parsed
response, call
ready(). In Qite, you will usually do this inside a component
method and then update fields (or publish an event) based on the result.
Example: shipment tracking lookup.
import Ajax from "/vendor/qite/src/lib/ajax.js";
class TrackingLookupComponent extends BaseComponent {
constructor(tree_node) {
super(tree_node);
this.events.add([
["@click", "#lookup", () => this.lookup()]
]);
}
async lookup() {
let tracking_id = this.fields.get("tracking_id");
let resp = await Ajax.get("/shipping/track", {
tracking_id: tracking_id
}).ready();
if (!resp.ok) {
this.fields.set({ error: "Lookup failed" });
return;
}
this.fields.set({
status: resp.data.status,
eta: resp.data.eta
});
}
}
resp
is a plain object stored as
ajax.resp
internally, returned for
convenience by
ready(). If you do need the Ajax instance (for request/response
snapshots or handler tables), keep it:
let ajax = Ajax.get("/shipping/track", { tracking_id: tracking_id });
let resp = await ajax.ready();
console.log(ajax.req, ajax.resp);
What you get back
When the request finishes,
ajax.resp
is populated with these fields:
-
ok— boolean, same meaning asResponse.ok -
status,status_text,url -
headers— lowercased header map -
content_type— convenience alias forheaders["content-type"] -
data— parsed response payload (based on response_type) -
raw— raw response string for text/html modes, otherwise null -
id— request id (useful in logs)
The request side is tracked in
ajax.req(method, url, headers, body), mostly
for debugging.
Request methods
Ajax exposes standard helpers:
Ajax.get(url, data, opts)
Ajax.post(url, data, opts)
Ajax.put(url, data, opts)
Ajax.patch(url, data, opts)
Ajax.delete(url, data, opts)
For
GET
and
HEAD
requests,
data
is encoded into the query string and
existing query params are preserved. For all other methods,
data
becomes the
request body (encoded based on
request_type).
// GET appends query params
await Ajax.get("/invoices?sort=date", { page: 2 }).ready();
// POST sends a body
await Ajax.post("/invoices", { amount_cents: 1200 }).ready();
Global defaults and per-request overrides
Ajax.configure(...)
sets defaults used by all future requests.
You can still override any option per request by passing
opts
to get/post/etc.
Ajax.configure({
headers: { "X-Requested-With": "XMLHttpRequest" },
request_type: "json",
response_type: "json"
});
// Override for one request
await Ajax.get("/report.csv", null, {
response_type: "text"
}).ready();
Configuration merges:
-
headersare merged deeply (defaults plus overrides) - handler tables are merged deeply (defaults plus overrides)
-
debugandthrow_onbecome arrays (single values are wrapped)
Request types
request_type
controls how the request body is encoded and which
Content-Type
is sent. For convenience and to make things concise,
Ajax
offers built-in request aliases:
-
"json"— JSON.stringify(data), Content-Type application/json -
"form"— URL-encoded body (a=1&b=2), Content-Type application/x-www-form-urlencoded -
"multipart"— FormData (no explicit Content-Type, browser sets boundary) -
"text"— String(data), Content-Type text/plain -
"html"— String(data), Content-Type text/html
// URL-encoded form body
await Ajax.post("/session", {
email: "a@b.com",
pass: "secret"
}, {
request_type: "form"
}).ready();
However, you can still use fully-qualified content types:
request_type: "application/x-www-form-urlencoded"
Arrays are supported for
form
and query strings by repeating keys:
// -> tag=a&tag=b
await Ajax.get("/search", { tag: ["a", "b"] }).ready();
Response types
response_type
controls how the response body is parsed and which
Accept
header is set.
Ajax
offers the same aliases as for
request type
:
"json",
"html"
and
"text". Example:
let resp = await Ajax.get("/health", null, {
response_type: "text",
throw_on: []
}).ready();
console.log(resp.data); // "ok"
console.log(resp.raw); // "ok"
Content-Type mismatch checks
When
response_type
is
"json", Ajax checks that Content-Type looks like JSON.
It accepts
application/json
and vendor types like
application/vnd.x+json.
If the response Content-Type does not match
response_type
and
throw_on
contains
"wrong_response_type", an
AjaxError
is thrown early.
If you disable
"wrong_response_type", Ajax will still try to parse based on
response_type
and may throw a parse error later.
// Expecting json, but response is text/plain.
// This disables mismatch check, but parse will still fail.
await Ajax.get("/plain_text", null, {
response_type: "json",
throw_on: []
}).ready();
Content-Type mismatches usually indicate a backend bug, a misconfigured endpoint, or an unexpected redirect. Failing early makes these problems visible immediately instead of letting bad data propagate deeper into your UI logic.
Throw rules
By default,
Ajax
only throws on:
-
wrong response type (token:
"wrong_response_type" - selected HTTP status codes (defaults include "404" and "500")
Throwing is controlled by
opts.throw_on, which is a list of tokens:
- exact status: "404", "500"
- buckets: "4xx", "5xx"
- special: "non_2xx"
- special: "wrong_response_type"
// Treat any 4xx as an exception, but do not throw on mismatched content type.
let resp = await Ajax.post("/charge", { amount_cents: 1200 }, {
throw_on: ["4xx"]
}).ready();
If a throw happens, you get an
AjaxError
that may include helpful fields like
ajax_id,
status,
url, and
response(the parsed resp snapshot).
Throw rules let you define your error philosophy once. In some systems,
HTTP errors are part of normal control flow and should be handled by
status handlers. In others, they are exceptional and should bubble up.
throw_on
makes this explicit and consistent across the application.
Handlers: success_handlers and error_handlers
Sometimes you do not want exceptions and, instead, you want central handling. Ajax supports handler tables for both success and error responses:
-
success_handlersfor ok responses (2xx/3xx, based onResponse.ok -
error_handlersfor non-ok responses (4xx/5xx)
Match order is the following:
- exact status key, for example "200" or "404"
- bucket key, for example "2xx" or "4xx"
-
and then there's fallback key
"any"
Handlers receive the Ajax instance, so they can read
ajax.req
and
ajax.resp, and they can still throw if they want. Example:
Ajax.configure({
success_handlers: {
"2xx": (ajax) => console.log("ok:", ajax.resp.status)
},
error_handlers: {
"401": (ajax) => window.location.href = "/login",
"4xx": (ajax) => console.warn("client error:", ajax.resp.status, ajax.resp.data),
"any": (ajax) => console.error("unexpected error:", ajax.resp.status)
},
throw_on: [] // let handlers deal with it
});
Handler tables are a clean alternative to sprinkling try/catch everywhere. They are also a good match for UI code, where you often want to translate HTTP results into user-visible notifications or redirects.
CSRF support
Ajax
supports CSRF for typical backends by providing a header name and a token
getter function. CSRF headers are only applied to body methods (POST/PUT/PATCH/DELETE),
never to GET/HEAD. You can configure CSRF manually:
Ajax.configure({
csrf_header: "X-CSRF-Token",
csrf_getter: () => Ajax.metaContent("csrf-token")
});
Or use a template name:
Ajax.configure({ csrf: "Rails" });
Supported templates include: Rails, Django, Phoenix, Laravel, Spring, AspNet.
If CSRF is enabled but header name or token is missing, an
AjaxError
is thrown
before sending the request to keep failures obvious.
CSRF is intentionally opt-in. Qite cannot guess how your backend works, so you either configure it explicitly or choose a template.
Debug logging
You can enable debug logging with
debug: ["request"],
debug: ["response"],
or both.
Ajax
logs as collapsed console groups and includes
request/response snapshots.
Ajax.configure({
debug: ["request", "response"]
});
This is especially useful during integration work, because you can see the final URL, headers, body encoding, status, content type, and parsed payload.
204 No Content
For HTTP 204/205, Ajax sets:
-
resp.data = null -
resp.raw = null
and skips parsing, regardless of response_type.
A fuller example: checkout charge flow
This example combines common features in a real scenario: request/response type, CSRF, handler tables, and throw rules.
Ajax.configure({
csrf: "Rails",
request_type: "json",
response_type: "json",
error_handlers: {
"422": (ajax) => console.warn("invalid:", ajax.resp.data),
"401": (ajax) => window.location.href = "/login"
},
// still throw for server errors
throw_on: ["5xx", "wrong_response_type"]
});
async function chargeInvoice(invoice_id) {
try {
let resp = await Ajax.post("/billing/charge", {
invoice_id: invoice_id
}).ready();
if (resp.ok) {
console.log("charged:", resp.data);
}
} catch (err) {
// 5xx and parse/type issues land here
console.error(err);
}
}