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(), and api.delete() create mutation endpoints.
  • Good endpoint names are list, detail, search, create, update, and remove.
  • The first generic is the response type.
  • The second generic is the variables type, only when the endpoint needs input.
TypeScript
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 string when the endpoint needs one value.
  • Use an object type when the endpoint needs more than one value.
  • Mutation variables are passed to mutate(variables) or mutateAsync(variables).
TypeScript
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 baseUrl already contains the host.
TypeScript
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

  • query turns variables into URL search params.
  • body chooses what should be sent as the request body.
  • bodyType switches between JSON and FormData bodies.
  • headers adds headers for this endpoint call.
  • authMode controls whether this endpoint uses auth.
  • For non-GET requests, variables become the JSON body when no body mapper is provided.
TypeScript
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

  • undefined values are skipped.
  • null is sent as null.
  • 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, and URLSearchParams are passed through unchanged.
  • Do not set content-type manually for FormData; 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 into useQuery().
  • 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.
TypeScript
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 into useMutation().
  • fn(variables) runs the request directly.
  • Use onSuccess to invalidate related query keys after a successful mutation.
  • Mutations do not create query keys because TanStack Query does not cache them like queries.
TypeScript
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.

TypeScript
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});