Documentation
Endpoints
Define request paths, variables, mappers, query helpers, and mutation helpers.
Defining endpoints
Inside api.resource(), each property is one endpoint.
The property name should describe what the endpoint does.
Use api.get() for reads. Use api.post(), api.put(), api.patch(), or api.delete() for writes.
Key points
api.get()creates a query endpoint.api.post(),api.put(),api.patch(), andapi.delete()create mutation endpoints.- Good endpoint names are
list,detail,search,create,update, andremove. - The first generic is the response type.
- The second generic is the variables type, only when the endpoint needs input.
1type User = {2 id: string;3 name: string;4};5 6type SearchUsersInput = {7 q?: string;8 page?: number;9};10 11type UpdateUserInput = {12 id: string;13 body: {14 name: string;15 };16};17 18export const users = api.resource("users", {19 list: api.get<User[]>("/users"),20 search: api.get<User[], SearchUsersInput>("/users/search"),21 detail: api.get<User, string>((id) => `/users/${id}`),22 update: api.patch<User, UpdateUserInput>(({ id }) => `/users/${id}`),23});Response and variables types
Each endpoint can describe two types: what it returns, and what it needs as input.
The first generic is always the response type.
The second generic is the variables type. Add it only when callers must pass input.
Those variables are later passed to helpers like toQuery(), fn(), mutate(), and mutateAsync().
Key points
- Use only one generic when the endpoint has no variables, like
api.get<User[]>("/users"). - Use a simple type like
stringwhen the endpoint needs one value. - Use an object type when the endpoint needs more than one value.
- Mutation variables are passed to
mutate(variables)ormutateAsync(variables).
1const users = api.resource("users", {2 list: api.get<User[]>("/users"),3 detail: api.get<User, string>((id) => `/users/${id}`),4 search: api.get<User[], { q: string; page: number }>("/users"),5 create: api.post<User, { name: string; email: string }>("/users"),6});7 8users.list.toQuery(); // no variables9users.detail.toQuery("user-1"); // string variables10users.search.toQuery({ q: "jane", page: 1 });Static and dynamic paths
Every endpoint needs a path.
A static path is just a string, such as "/posts".
A dynamic path is a function. Use it when the URL needs a variable, such as a post ID.
The path function receives the same variables that you pass when you call the endpoint helper.
Key points
- Static paths work well for list and create endpoints.
- Dynamic paths work well for detail, update, and delete endpoints.
- Return only the path part when
baseUrlalready contains the host.
1const posts = api.resource("posts", {2 list: api.get<Post[]>("/posts"),3 detail: api.get<Post, string>((postId) => `/posts/${postId}`),4 comments: api.get<Comment[], { postId: string }>(5 ({ postId }) => `/posts/${postId}/comments`,6 ),7});Warning
Avoid manual query strings
Keep paths focused on the path part only. Prefer the query mapper instead of manually adding ?page=... to endpoint paths.
Endpoint request mappers
Sometimes variables are not only used in the path.
They may also become query params, a request body, request headers, or auth behavior.
Request mappers describe those parts in one place, next to the endpoint.
Key points
queryturns variables into URL search params.bodychooses what should be sent as the request body.bodyTypeswitches between JSON and FormData bodies.headersadds headers for this endpoint call.authModecontrols whether this endpoint uses auth.- For non-GET requests, variables become the JSON body when no
bodymapper is provided.
1type SearchUsersInput = {2 q?: string;3 page?: number;4 tags?: string[];5};6 7type UpdateUserInput = {8 id: string;9 body: {10 name: string;11 };12 traceId: string;13};14 15export const users = api.resource("users", {16 search: api.get<User[], SearchUsersInput>("/users", {17 query: ({ q, page, tags }) => ({ q, page, tags }),18 }),19 update: api.patch<User, UpdateUserInput>(20 ({ id }) => `/users/${id}`,21 {22 body: ({ body }) => body,23 headers: ({ traceId }) => ({24 "x-trace-id": traceId,25 }),26 authMode: "required",27 },28 ),29});Query serialization
Use the query mapper when an endpoint needs URL search params.
Return a plain object from the mapper. micro-rq turns that object into the final query string.
This keeps endpoint paths clean and avoids manually writing strings like ?page=1.
Key points
undefinedvalues are skipped.nullis sent asnull.- Arrays repeat the same key, for example
tags=a&tags=b. - Objects are converted with
JSON.stringify(). - Numbers and booleans are converted to strings.
Bodies and headers
Mutation endpoints often send data to the server.
By default, non-GET variables are sent as the JSON body.
Use a body mapper when only part of the variables should be sent as the body.
Headers can come from the API client, the endpoint, and auth. They are merged in that order.
Key points
- Plain objects are converted with
JSON.stringify(). FormData,Blob, andURLSearchParamsare passed through unchanged.- Do not set
content-typemanually forFormData; the browser adds the multipart boundary.
Info
JSON content-type
content-type: application/json is added for JSON bodies when no content type exists.
If you provide a content-type header yourself, micro-rq keeps your value.
Query endpoint helpers
GET endpoints are used for reading data.
They expose helpers that fit TanStack Query's useQuery() API.
They also expose key helpers, so you can invalidate or refetch the right cached data.
Key points
toQuery()returns the object you spread intouseQuery().key()returns the exact query key for one endpoint call.baseKey()returns the shared key prefix for broader invalidation.fn()gives you the request function when you want to call it yourself.
1const userQuery = useQuery({2 ...users.detail.toQuery("user-1"),3 staleTime: 60_000,4});5 6queryClient.invalidateQueries({7 queryKey: users.detail.key("user-1"),8});9 10queryClient.invalidateQueries({11 queryKey: users.list.baseKey(),12});13 14const loadUser = users.detail.fn("user-1");15const user = await loadUser();Mutation endpoint helpers
POST, PUT, PATCH, and DELETE endpoints are used for changing data.
They expose helpers that fit TanStack Query's useMutation() API.
Unlike queries, mutation variables are passed when you call mutate() or mutateAsync().
Key points
toMutation()returns the object you spread intouseMutation().fn(variables)runs the request directly.- Use
onSuccessto invalidate related query keys after a successful mutation. - Mutations do not create query keys because TanStack Query does not cache them like queries.
1const updateUser = useMutation({2 ...users.update.toMutation(),3 onSuccess: (user) => {4 queryClient.invalidateQueries({5 queryKey: users.detail.key(user.id),6 });7 },8});9 10updateUser.mutate({11 id: "user-1",12 body: {13 name: "Jane",14 },15 traceId: crypto.randomUUID(),16});17 18await users.update.fn({19 id: "user-1",20 body: {21 name: "Jane",22 },23 traceId: "manual-call",24});Complete resource example
This example puts the pieces together in one resource.
It includes a list query, a detail query, an update mutation, query params, request body mapping, endpoint headers, auth mode, and invalidation.
1// api/resources/products.ts2import { api } from "../client";3 4export type Product = {5 id: string;6 title: string;7 price: number;8};9 10export type ProductFilters = {11 q?: string;12 category?: string;13 page?: number;14};15 16export type UpdateProductInput = {17 id: string;18 body: {19 title?: string;20 price?: number;21 };22 requestId: string;23};24 25export const products = api.resource("products", {26 list: api.get<Product[], ProductFilters>("/products", {27 query: (filters) => filters,28 }),29 detail: api.get<Product, string>((id) => `/products/${id}`),30 update: api.patch<Product, UpdateProductInput>(31 ({ id }) => `/products/${id}`,32 {33 body: ({ body }) => body,34 headers: ({ requestId }) => ({35 "x-request-id": requestId,36 }),37 authMode: "required",38 },39 ),40});41 42// components/ProductDetail.tsx43const productQuery = useQuery({44 ...products.detail.toQuery(productId),45 enabled: Boolean(productId),46});47 48const updateProduct = useMutation({49 ...products.update.toMutation(),50 onSuccess: (product) => {51 queryClient.invalidateQueries({52 queryKey: products.detail.key(product.id),53 });54 55 queryClient.invalidateQueries({56 queryKey: products.list.baseKey(),57 });58 },59});