The fetch client returns standard promises, so it fits naturally into whatever async pattern your framework uses.
// React: pages/users/[id]/index.tsx
import { useLoaderData } from "react-router";
import fetchClients from "_/fetch";
const { GET } = fetchClients["users/[id]"];
// React Router passes { params, request } into the loader; read params the
// native way and pass them to the client as an array
export const loader = ({ params }) => GET([params.id]);
export default function UserProfile() {
const user = useLoaderData();
return <div>{user.name}</div>;
}Types flow through these abstractions - loaders, resources, hooks, and components automatically know the response shape from your API definition.
KosmoJS only owns the envelope - the typed fetch client and how requests cross the wire. Everything above it is the framework's own model: Solid's query/createAsync, React Router's loader and useLoaderData, Vue's useLoaderData, Svelte's useLoaderData, MDX's useLoaderData - each reads through its native patterns, with no proprietary abstraction layered on top.
The client is just a typed function that takes the params array you build and returns a promise; where and how you call it is entirely the framework's.
These Integrations Work Under SSR Too
The client returns standard promises, so nothing above changes when the page is server-rendered.
What matters is when the fetch fires: loaders and preloaded resources run during the SSR render, so they take the in-process path and their result is reused on hydration rather than refetched.
A fetch in useEffect / onMounted does not run during SSR - it fetches in the browser after hydration, like a plain CSR app.
Suspense Is Your Responsibility
Solid's createAsync (like createResource) suspends: it reports its pending state to the nearest <Suspense> boundary and propagates errors to the nearest <ErrorBoundary>.
KosmoJS does not provide either for you - the seeded App boilerplate renders its children directly, deliberately not wrapping the app in <Suspense>, because one app-wide boundary is an anti-pattern: any pending fetch anywhere collapses the whole page to a single fallback and unrelated async work shares one loading state.
Scope the boundary to the data component or a sensible subtree yourself:
import { Suspense } from "solid-js";
import { createAsync, useParams } from "@solidjs/router";
export default function UserProfile() {
const params = useParams();
const user = createAsync(() => getUser(params.id));
return (
<Suspense fallback={<div>Loading...</div>}>
<div>{user()?.name}</div>
</Suspense>
);
}React's loader/useLoaderData resolves before render and does not suspend, so it needs no boundary unless you reach for React.lazy or a promise-throwing use().
The same holds for Vue, Svelte, and MDX loaders - they resolve before render, so only Solid's createAsync needs a boundary in the common case.
Wrapping the whole app in one boundary does work if you accept the tradeoff - it is your call, not a default KosmoJS makes for you.
See Data Preloading for the full breakdown.