Define REST endpoints once and use them with TanStack Query without wrapping TanStack Query.
Docs: https://micro-rq-docs.vercel.app/
TanStack Query is excellent at caching, background refetching, retries, mutations, invalidation, and async server-state orchestration. In REST apps, the repeated work is usually not TanStack Query itself. It is the code around it: building URLs, serializing query params, creating stable query keys, adding auth headers, parsing responses, refreshing tokens, and keeping request functions consistent across screens.
micro-rq is a small resource builder for that surrounding REST layer. You describe each REST endpoint once, then use the generated output directly with TanStack Query.
It gives you:
- stable query keys for invalidation and exact cache targeting
- request functions for direct calls and tests
useQuery-ready configs withqueryKeyandqueryFnuseMutation-ready configs withmutationFn- shared base URLs, headers, auth modes, token refresh, response parsing, and error handling
It is useful when your app already uses TanStack Query and you want a typed, consistent REST layer without creating another hook abstraction.
It does not generate React hooks, replace TanStack Query, implement caching, or hide TanStack Query options. You still call useQuery, useMutation, invalidateQueries, and pass options such as enabled, staleTime, select, and onSuccess yourself.
npm install micro-rq @tanstack/react-query@tanstack/react-query is a peer dependency.
import { createMicroApi } from "micro-rq";
export const api = createMicroApi({
name: "main",
baseUrl: "/api",
});name is included in query keys. This keeps keys from different APIs separate.
type User = {
id: string;
name: string;
email: string;
};
type CreateUserDto = {
name: string;
email: string;
};
export const users = api.resource("users", {
list: api.get<User[], { page: number }>("/users", {
query: (params) => params,
}),
detail: api.get<User, string>((id) => `/users/${id}`),
create: api.post<User, CreateUserDto>("/users"),
});GET endpoints become query endpoints. POST, PUT, PATCH, and DELETE endpoints become mutation endpoints.
const usersQuery = useQuery({
...users.list.toQuery({ page: 1 }),
staleTime: 60_000,
});
const userQuery = useQuery({
...users.detail.toQuery(userId),
enabled: Boolean(userId),
});toQuery() returns only:
{
queryKey,
queryFn,
}const createUser = useMutation({
...users.create.toMutation(),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: users.list.baseKey(),
});
},
});
createUser.mutate({
name: "John",
email: "john@example.com",
});toMutation() returns only:
{
mutationFn,
}Mutation variables are passed to mutate(), not to toMutation().
Keys follow this shape:
[apiName, resourceName, endpointName, variables?]users.list.baseKey();
// ["main", "users", "list"]
users.list.key({ page: 1 });
// ["main", "users", "list", { page: 1 }]
users.detail.key("user-1");
// ["main", "users", "detail", "user-1"]Use baseKey() when you want to invalidate every query for one endpoint:
queryClient.invalidateQueries({
queryKey: users.list.baseKey(),
});Use key(input) when you want one exact query key.
Paths can be static or dynamic:
api.get<User[]>("/users");
api.get<User, string>((id) => `/users/${id}`);Use mappers when request variables do not match the final request directly:
api.get<User[], { page: number; search?: string }>("/users", {
query: (params) => params,
});
api.patch<User, { id: string; body: Partial<User> }>(
({ id }) => `/users/${id}`,
{
body: ({ body }) => body,
},
);Query serialization rules:
undefinedvalues are ignored.nullbecomes"null".- arrays repeat keys, for example
?tags=a&tags=b - objects are JSON-stringified.
For non-GET methods, variables are sent as the JSON body by default unless a body mapper is provided. GET requests never send a body.
import { createMicroApi, createTokenProvider } from "micro-rq";
type AuthTokens = {
accessToken: string;
refreshToken: string;
};
const authApi = createMicroApi({
name: "auth",
baseUrl: "/api",
});
const auth = authApi.resource("auth", {
refresh: authApi.post<AuthTokens, { refreshToken?: string | null }>("/refresh", {
authMode: "none",
}),
});
const tokenProvider = createTokenProvider({
getAccessToken: () => localStorage.getItem("accessToken"),
getRefreshToken: () => localStorage.getItem("refreshToken"),
refresh: {
fn: ({ refreshToken }) => auth.refresh.fn({ refreshToken }),
selectAccessToken: (tokens) => tokens.accessToken,
onSuccess: (tokens) => {
localStorage.setItem("accessToken", tokens.accessToken);
localStorage.setItem("refreshToken", tokens.refreshToken);
},
onError: () => {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
},
},
});
export const api = createMicroApi({
name: "main",
baseUrl: "/api",
tokenProvider,
authHeader: (token) => ({
Authorization: `Bearer ${token}`,
}),
});If a request returns 401 and refresh is configured, micro-rq refreshes once and retries the original request once. Parallel 401 responses share the same refresh promise.
Endpoint auth modes:
optional: default. Use a token when one exists.none: skip token lookup, auth header injection, and refresh-on-401.required: require an access token before callingfetch; throwsMicroAuthRequiredErrorif missing.
Failed HTTP responses throw MicroApiError.
import { MicroApiError } from "micro-rq";
try {
await users.detail.fn("user-1")();
} catch (error) {
if (error instanceof MicroApiError) {
console.log(error.status);
console.log(error.statusText);
console.log(error.data);
console.log(error.response);
}
}You can also observe failures at the API-client level:
const api = createMicroApi({
name: "main",
baseUrl: "/api",
onError: (error, context) => {
console.log(context.method, context.url, error);
},
});The original error is still thrown so TanStack Query retries, error state, callbacks, and error boundaries keep working normally.
Use generated query configs with TanStack Query's prefetchQuery, then hydrate for Client Components.
// app/products/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
import { products } from "../api/resources/products";
import { ProductsClient } from "./products-client";
export default async function ProductsPage() {
const queryClient = new QueryClient();
const params = { limit: 12, skip: 0 };
await queryClient.prefetchQuery(products.list.toQuery(params));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductsClient params={params} />
</HydrationBoundary>
);
}// app/products/products-client.tsx
"use client";
import { useQuery } from "@tanstack/react-query";
import { products } from "../api/resources/products";
export function ProductsClient({ params }: { params: { limit: number; skip: number } }) {
const productsQuery = useQuery({
...products.list.toQuery(params),
});
// Render productsQuery.data.
}Inputs and outputs are inferred from endpoint definitions.
const q = users.detail.toQuery("user-1");
// q.queryFn returns Promise<User>
const m = users.create.toMutation();
// m.mutationFn accepts CreateUserDto and returns Promise<User>These fail type checking:
users.detail.toQuery(123);
users.create.fn({ wrong: "field" });No-variable endpoints do not require undefined:
const me = api.resource("me", {
get: api.get<User>("/me"),
});
me.get.toQuery();
me.get.key();
me.get.fn();The root package export is the public API.
Runtime exports:
createMicroApicreateTokenProviderMicroApiErrorMicroAuthRequiredError
Type exports:
MicroApiCreateMicroApiConfigMicroRequestContextTokenProviderTokenProviderConfigRefreshTokenConfigBuiltResourceQueryEndpointMutationEndpointQueryConfigMutationConfigMicroQueryKeyVariablesArgsAuthModeBodyTypeHttpMethodMaybePromisePathBuilderRequestMappers
Example app:
cd examples/next
npm install
npm run devDocs app source:
cd docs
npm install
npm run devmicro-rq publishes version-matched agent docs at node_modules/micro-rq/dist/docs/.
Run this command in your project root to add the managed AGENTS.md block:
npx micro-rq agents initOr add this block to your project's root AGENTS.md manually so coding agents read the installed package docs before changing micro-rq code:
<!-- BEGIN:micro-rq-agent-rules -->
# micro-rq: ALWAYS read docs before coding
Before any micro-rq work, find and read the relevant doc in `node_modules/micro-rq/dist/docs/`. The installed package docs are the source of truth for this project's micro-rq version.
Use micro-rq to define REST resources once and pass generated query/mutation configs directly to TanStack Query. Do not wrap TanStack Query, generate hooks, or invent APIs that are not documented in the bundled docs.
<!-- END:micro-rq-agent-rules -->npm run release:check
npm publishrelease:check runs typecheck, tests, type tests, build, and npm pack --dry-run.
Published files are limited to dist, README.md, CHANGELOG.md, and LICENSE.