How htmx Constructs AJAX Requests: A Deep Dive into the Source Code

htmx constructs AJAX requests through a four-stage pipeline in src/htmx.js that normalizes context, builds headers and payload, manages the XMLHttpRequest lifecycle, and processes responses.

The bigskysoftware/htmx library handles all network communication through a centralized request builder that bridges declarative HTML attributes and programmatic JavaScript calls. Understanding how htmx constructs AJAX requests reveals why the library can swap HTML fragments with minimal boilerplate while maintaining full control over headers, credentials, and request bodies.

The Public API: htmx.ajax and ajaxHelper

All programmatic AJAX requests flow through htmx.ajax, which the library exposes as the ajaxHelper function at line 307 in src/htmx.js.

// The public method is attached directly to the htmx object
htmx.ajax = ajaxHelper;   // src/htmx.js line 307

The function signature accepts three parameters: an HTTP verb, a URL path, and an optional context that can be a DOM element, CSS selector, or configuration object. When invoked, ajaxHelper (defined at lines 4059‑4092) normalizes the verb via verb.toLowerCase() and resolves the target element using resolveTarget. If the context is an object, htmx extracts configuration fields—including source, target, headers, values, swap, push, and replace—and packages them into a request configuration object.

Request Construction Pipeline

The internal issueAjaxRequest function orchestrates the actual network call by delegating to four distinct responsibilities.

1. Context Resolution and Normalization

Before opening the connection, htmx determines exactly which element triggered the request and where the response should land. The resolveTarget function converts CSS selectors or direct element references into concrete DOM nodes. If a target is specified in the context object, it becomes the targetOverride for the response phase. This resolution step ensures that subsequent pipeline stages operate on verified DOM references rather than raw strings.

2. Header and Payload Assembly

htmx builds request metadata through getHeaders (lines 4620‑4660) and gathers form data via getInputValues (lines 3780‑3810).

The header construction automatically injects:

  • HX-Request: true (identifies the request as originating from htmx)
  • X-Requested-With: XMLHttpRequest (standard AJAX marker)
  • Custom headers from the hx-headers attribute or the context object’s headers property
  • CSRF/anti‑forgery tokens when the hx-csrf attribute is present

For the request body, getInputValues serializes form fields, hx-vals, and hx-prompt values. Depending on the verb and Content-Type, htmx encodes the payload as FormData or JSON. Non‑GET verbs receive the encoded data via xhr.send(body), while GET requests append parameters to the URL through normalizePath (lines 4460‑4560).

3. XHR Lifecycle Management

Inside issueAjaxRequest, htmx instantiates a native XMLHttpRequest and applies security and configuration policies:

  • Domain restrictions: The global selfRequestsOnly config (lines 1919‑1925) prevents requests to external origins
  • Credentials: The withCredentials setting (lines 1880‑1886) controls cookie and authorization header transmission

Before calling xhr.open(verb, url, true), htmx fires the htmx:configRequest event to allow extensions to mutate the XHR object. The htmx:beforeRequest event immediately follows; returning false from a handler aborts the request entirely. Once cleared, htmx sets the assembled headers with xhr.setRequestHeader and transmits the payload.

4. Response Processing and DOM Swapping

After the server responds, processResponse handles the result. The response text is parsed into DOM fragments via makeFragment (lines 6063‑6145), which extracts any <title> tags for history updates. The getSwapSpecification function determines the insertion strategy (e.g., innerHTML, outerHTML, beforebegin), then swap (lines 6790‑6980) performs the actual DOM manipulation, respecting hx-swap, hx-select, and hx-select-oob attributes.

Once the DOM update completes, htmx fires htmx:afterSwap and htmx:afterRequest, followed by internal cleanup via cleanUpElement to remove temporary state from processed elements.

Practical Code Examples

You can trigger htmx AJAX requests either declaratively or programmatically.

<!-- Declarative request targeting a specific DOM node -->
<button hx-get="/search?q=hello" hx-target="#results" hx-swap="innerHTML">
  Search
</button>

The programmatic equivalent uses the same pipeline:

// Basic programmatic request
htmx.ajax('GET', '/search?q=hello', { target: '#results' });

For advanced use cases, pass a configuration object to override headers, inject additional values, and control history:

htmx.ajax('POST', '/api/items', {
  source: '#new-item-form',           // Element providing form values
  headers: { 'X-My-Header': '123' },  // Custom request headers
  values: { extra: 'data' },          // Additional payload merged with form data
  swap: 'innerHTML',                  // Swap style override
  push: true                          // Push URL to browser history
});

Summary

  • Context resolution: ajaxHelper and resolveTarget convert selectors and configuration objects into concrete DOM references and request parameters.
  • Header and payload creation: getHeaders assembles standard and custom headers, while getInputValues encodes form data as FormData or JSON.
  • XHR lifecycle: issueAjaxRequest creates the XMLHttpRequest, enforces selfRequestsOnly security, applies withCredentials, and dispatches htmx:configRequest and htmx:beforeRequest events.
  • Response processing: makeFragment parses HTML, swap updates the DOM according to hx-swap specifications, and lifecycle events signal completion.

Frequently Asked Questions

What is the main entry point for programmatic AJAX in htmx?

The htmx.ajax method, which aliases the internal ajaxHelper function at line 307 of src/htmx.js, serves as the primary JavaScript API. It accepts an HTTP verb, URL, and optional context object, then delegates to issueAjaxRequest to execute the network call.

How does htmx handle CSRF tokens in AJAX requests?

The getHeaders function (lines 4620‑4660) checks for the hx-csrf attribute and automatically appends the token to the request headers. You can also supply tokens manually via the headers property in the context object passed to htmx.ajax.

Can I cancel or modify an AJAX request before it is sent?

Yes. Listen for the htmx:configRequest event to modify the underlying XMLHttpRequest before it opens, or handle htmx:beforeRequest to abort the request by returning false. Both hooks fire inside issueAjaxRequest before xhr.send() is called.

What determines whether htmx sends FormData or JSON in the request body?

The encoding depends on the HTTP verb and the Content-Type header. getInputValues (lines 3780‑3810) gathers form fields and hx-vals, then issueAjaxRequest encodes the payload as FormData by default or as JSON when the Content-Type indicates JSON format. GET requests never include a body; parameters are appended to the URL via normalizePath.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →