createFetcher
createFetcher is the framework-agnostic core of use-q. It takes the same Schema you’d use with the React layer and returns a fetch function that’s fully typed end-to-end. Use it anywhere a QueryClient would be overkill — Node scripts, edge workers, server actions, React Server Components, route loaders.
import { createFetcher } from "@use-q/api-client";import { schema } from "./schema";
const fetcher = createFetcher<typeof schema>(schema, { baseUrl: "https://api.example.com",});
const posts = await fetcher.fetch("listPosts", { pathParams: { facilityId: "f1" },});// ^? Post[]Returned shape
Section titled “Returned shape”createFetcher returns a FetcherInstance with one method:
fetcher.fetch(routeName, { pathParams?, searchParams?, body?, signal?, headers?, // per-call overrides});The return type is the schema’s response for that route. Errors throw an ApiError (or your custom parseError output) — see Error handling.
Options
Section titled “Options”baseUrl
Section titled “baseUrl”Prepended to every request path. Trailing slashes are normalized.
createFetcher(schema, { baseUrl: "https://api.example.com/v1" });fetchFn
Section titled “fetchFn”Inject a custom fetch implementation. Useful for tests, Node 16, or wrapping fetch with retry/auth/logging middleware.
import { fetch as undiciFetch } from "undici";
const fetcher = createFetcher(schema, { baseUrl: "https://api.example.com", fetchFn: undiciFetch,});defaultHeaders
Section titled “defaultHeaders”Headers attached to every request. Can be a static object or an async function — the function is awaited on every call, so it’s a great place to read a token from a store.
// StaticcreateFetcher(schema, { baseUrl: "https://api.example.com", defaultHeaders: { "x-api-version": "2026-01-01", },});
// Dynamic / asynccreateFetcher(schema, { baseUrl: "https://api.example.com", defaultHeaders: async () => ({ Authorization: `Bearer ${await getAccessToken()}`, "x-tenant-id": currentTenantId(), }),});Per-call headers overrides win over defaults:
await fetcher.fetch("getPost", { pathParams: { facilityId: "f1", postId: "p1" }, headers: { "x-debug": "1" },});parseError
Section titled “parseError”Convert error responses into your domain’s error type. parseError runs only for non-2xx responses and receives the raw Response and parsed JSON (if any).
interface MyApiError { code: string; detail: string; fieldErrors?: Record<string, string>;}
const fetcher = createFetcher<typeof schema, MyApiError>(schema, { baseUrl: "https://api.example.com", parseError: async (res) => { const body = (await res.json().catch(() => null)) as MyApiError | null; return { code: body?.code ?? "unknown", detail: body?.detail ?? res.statusText, fieldErrors: body?.fieldErrors, }; },});
try { await fetcher.fetch("createPost", { pathParams: { facilityId: "f1" }, body: { title: "" }, });} catch (err) { if (isApiError<MyApiError>(err)) { console.error(err.parsed.code, err.parsed.fieldErrors); }}onError
Section titled “onError”A side-effecting hook called for every failure (after parseError). Useful for telemetry or auth fan-out.
import { isApiError } from "@use-q/api-client";
createFetcher(schema, { baseUrl: "https://api.example.com", onError: (err, ctx) => { if (isApiError(err) && err.status === 401) { logoutAndRedirect(); } telemetry.captureException(err, { tags: { route: ctx.routeName, method: ctx.method }, }); },});The second argument is a context object: { routeName, method, url, status }.
Recipes
Section titled “Recipes”Node CLI
Section titled “Node CLI”#!/usr/bin/env nodeimport { createFetcher } from "@use-q/api-client";import { schema } from "./schema.js";
const fetcher = createFetcher<typeof schema>(schema, { baseUrl: process.env.API_BASE_URL!, defaultHeaders: () => ({ Authorization: `Bearer ${process.env.API_TOKEN!}`, }),});
const posts = await fetcher.fetch("listPosts", { pathParams: { facilityId: process.argv[2]! },});
console.table(posts.map(({ id, title }) => ({ id, title })));Cloudflare Worker
Section titled “Cloudflare Worker”import { createFetcher } from "@use-q/api-client";import { schema } from "./schema";
export default { async fetch(req: Request, env: Env): Promise<Response> { const fetcher = createFetcher<typeof schema>(schema, { baseUrl: env.API_BASE_URL, fetchFn: fetch, // Workers global fetch defaultHeaders: { "x-internal": env.INTERNAL_KEY }, });
const posts = await fetcher.fetch("listPosts", { pathParams: { facilityId: "f1" }, }); return Response.json(posts); },} satisfies ExportedHandler<Env>;Next.js Server Action
Section titled “Next.js Server Action”"use server";
import { createFetcher } from "@use-q/api-client";import { cookies } from "next/headers";import { schema } from "@/api/schema";
const fetcher = createFetcher<typeof schema>(schema, { baseUrl: process.env.API_BASE_URL!, defaultHeaders: async () => ({ Authorization: `Bearer ${cookies().get("token")?.value ?? ""}`, }),});
export async function createPost(facilityId: string, formData: FormData) { return fetcher.fetch("createPost", { pathParams: { facilityId }, body: { title: String(formData.get("title")), body: String(formData.get("body")), }, });}React Router data loader
Section titled “React Router data loader”import { createFetcher } from "@use-q/api-client";import type { LoaderFunctionArgs } from "react-router-dom";import { schema } from "./schema";
const fetcher = createFetcher<typeof schema>(schema, { baseUrl: import.meta.env.VITE_API_BASE_URL,});
export async function postsLoader({ params, request }: LoaderFunctionArgs) { return fetcher.fetch("listPosts", { pathParams: { facilityId: params.facilityId! }, signal: request.signal, });}Why not just use fetch?
Section titled “Why not just use fetch?”You get four things fetch doesn’t give you:
- Static guarantees. Misspell a route name, miss a path param, send the wrong body shape — TypeScript catches it.
- URL building. Path placeholders are filled,
undefinedsearch params are dropped, JSON bodies are serialized. - Consistent error shape. Non-2xx responses always throw an
ApiError(or yourparseErroroutput). - A swap-in path to React. The same schema works with
createApiClientfor hooks and cache.