Fetching Data in Mithril SPAs with m.request: A Practical Guide
How to fetch and post JSON in a Mithril SPA using m.request: correct config keys, explicit HTTP error handling with extract, and what Mithril deliberately does not manage for you.
16 Sept 2026, 03:04 UTC

What you are trying to achieve
Your Mithril single-page app needs to load JSON from an API and re-render when the data arrives, without pulling in axios or writing XHR boilerplate. Mithril ships with m.request, a thin promise-based wrapper over XHR that parses JSON automatically and triggers a redraw when the promise settles. This guide shows the minimal correct pattern: issuing requests from route components, handling errors explicitly, and verifying the behavior in devtools.
Prerequisites
- Mithril 2.x loaded in the page (the API described here is stable across the 2.x line).
- A backend endpoint that returns JSON and sends CORS headers if it lives on a different origin.
- Basic familiarity with Mithril components and
m.route.
Issuing a request
m.request takes a config object and returns a promise that resolves with the already-parsed response body — no JSON.parse and no response.json() step:
var UserList = {
users: [],
error: null,
oncreate: function() {
m.request({
method: "GET",
url: "/api/users"
}).then(function(data) {
UserList.users = data;
}).catch(function(e) {
UserList.error = "Could not load users";
});
},
view: function() {
if (UserList.error) return m("p.error", UserList.error);
return m("ul", UserList.users.map(function(u) {
return m("li", u.name);
}));
}
};Run this in the browser as part of a routed component, e.g. m.route(document.body, "/users", {"/users": UserList}). No special permissions are needed beyond what your API requires. When the promise resolves, Mithril schedules a redraw automatically, so the list appears without calling m.redraw() yourself.
Sending data
For writes, set method and data. Mithril serializes plain objects to JSON and sets the Content-Type: application/json header for you:
m.request({
method: "POST",
url: "/api/users",
body: {name: "Ada"}
}).then(function(created) {
// created is the parsed response body
});Note the key is body in Mithril 2.x; older 1.x code used data. If you are on 1.x, check which your version expects before debugging a mysteriously empty request payload.
Handling HTTP errors correctly
One behavior trips people up: m.request rejects on network failures and on non-2xx HTTP statuses, but the rejection value is an Error whose code property holds the HTTP status — the response body is not automatically included. If your API returns useful error payloads, use the extract option to capture them:
m.request({
method: "POST",
url: "/api/users",
body: payload,
extract: function(xhr) {
return {status: xhr.status, body: JSON.parse(xhr.responseText)};
}
}).then(function(res) {
if (res.status >= 400) {
// handle validation messages from res.body
}
});Without extract, a 422 response becomes a generic rejection and you lose the server's error detail. Always have a .catch (or a status check via extract) that surfaces something to the user; a silently failing request leaves the UI showing stale or empty state forever.
What Mithril does not do for you
- No automatic cancellation. If a component issues a request and the user navigates away before it resolves, the promise still settles and your callback still runs. Guard assignments with a flag (e.g. check a token set in
onremove) if late updates cause problems. - No retry, debounce, or deduplication. Rapidly changing inputs (search-as-you-type) will fire a request per keystroke unless you debounce yourself.
- CORS is the server's job. If the API is cross-origin and lacks
Access-Control-Allow-Origin, the browser blocks the response no matter what you configure client-side.
Verifying the result
Open browser devtools and check the Network tab: confirm each m.request call appears with the expected method, URL, and — for POST — a JSON request payload. Then simulate failure: use devtools' network throttling "Offline" mode, or point the URL at a nonexistent route, and confirm your .catch runs and the error message renders. Finally, navigate away mid-request and confirm no console errors or unexpected state updates occur after the component is removed.
Summary
Use m.request from oncreate (or route resolvers) with method, url, and body; rely on automatic JSON parsing and redraws; add extract when you need error response bodies; and handle cancellation, debouncing, and CORS explicitly — Mithril deliberately leaves those to you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.