npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

micro-rq

v0.3.0

Published

Define REST resources once and generate TanStack Query configs without wrapping TanStack Query.

Readme

micro-rq

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 with queryKey and queryFn
  • useMutation-ready configs with mutationFn
  • 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.

Install

npm install micro-rq @tanstack/react-query

@tanstack/react-query is a peer dependency.

Upgrading from 0.2.x

This upgrade has no breaking changes. Existing bearer-token configurations that use createTokenProvider continue to work without changes.

If your backend uses HttpOnly cookies, you no longer need to create a token provider only to run the refresh request. Configure refresh directly on the API:

const api = createMicroApi({
  name: "main",
  baseUrl: "/api",
  refresh: {
    fn: () => auth.refresh.fn(),
  },
});

The direct refresh operation can return no data, including a 204 No Content response. Configure refresh either on the API or in tokenProvider, not both.

Upgrading from 0.1.x

Version 0.2.0 removes refresh.selectAccessToken. Save refreshed tokens in refresh.onSuccess; micro-rq waits for it before retrying the original request.

refresh: {
  fn: ({ refreshToken }) => auth.refresh.fn({ refreshToken }),
- selectAccessToken: (tokens) => tokens.accessToken,
  onSuccess: (tokens) => {
    localStorage.setItem("accessToken", tokens.accessToken);
    localStorage.setItem("refreshToken", tokens.refreshToken);
  },
}

The provider no longer keeps its own access-token copy. getAccessToken is now the source of truth, so removing the token from application storage takes effect on the next request.

Quick Start

Step 1: Create an API client

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.

Step 2: Define a resource

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.

Step 3: Use query 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,
}

The generated queryFn forwards TanStack Query's AbortSignal to fetch, so cancelling a query also cancels the underlying HTTP request.

Step 4: Use mutation endpoints

const createUser = useMutation({
  ...users.create.toMutation(),
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: users.list.baseKey(),
    });
  },
});

createUser.mutate({
  name: "John",
  email: "[email protected]",
});

toMutation() returns only:

{
  mutationFn,
}

Mutation variables are passed to mutate(), not to toMutation().

Infinite Queries

Use toInfiniteQuery() with TanStack Query's useInfiniteQuery:

const postsQuery = useInfiniteQuery({
  ...posts.list.toInfiniteQuery({
    initialPageParam: 0,
    keyVariables: {
      tag,
      limit: 20,
    },
    getVariables: ({ pageParam, keyVariables }) => ({
      skip: pageParam,
      ...keyVariables,
    }),
  }),
  getNextPageParam: (lastPage) => {
    const nextSkip = lastPage.skip + lastPage.limit;
    return nextSkip < lastPage.total ? nextSkip : undefined;
  },
});

getVariables converts any page-number, offset, or cursor pageParam into the endpoint's variables. keyVariables holds stable filters and page size so different infinite lists do not share cached data. The generated key uses:

[apiName, resourceName, endpointName, "infinite", keyVariables | null]

The page parameter is not part of the key because TanStack Query stores every page under one infinite-query entry. getNextPageParam remains a TanStack Query option because the API response decides where the next page comes from. toInfiniteQuery() also forwards TanStack Query's cancellation signal to fetch.

Query Keys

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.

Request Mapping

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:

  • undefined values are ignored.
  • null becomes "null".
  • arrays repeat keys, for example ?tags=a&tags=b
  • objects are JSON-stringified.
  • existing query parameters in the endpoint path are preserved.

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.

Response Parsing

Use the optional parse mapper to validate or transform successful responses:

api.get<User>("/users/1", {
  parse: (data) => userSchema.parse(data),
});

The parser receives unknown and can return a value directly or a promise. Use any validation library or a custom function; micro-rq does not include a validation dependency. The parser runs only for successful HTTP responses. If it throws, onError observes the same error once and the caller receives it.

Auth and Refresh

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 }),
    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.

onSuccess is awaited before the original request is retried. Save the new tokens there so getAccessToken returns the new access token for the retry. The token provider does not keep its own copy, so clearing your application's token storage also logs out future requests.

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 calling fetch; throws MicroAuthRequiredError if missing.

HttpOnly Cookie Sessions

For same-origin cookie sessions, the browser handles Set-Cookie and sends the cookie on later requests. No tokenProvider or authHeader is needed:

const api = createMicroApi({
  name: "main",
  baseUrl: "/api",
});

For a cross-origin API, use a custom fetcher with credentials:

const cookieFetcher: typeof fetch = (input, init) =>
  fetch(input, {
    ...init,
    credentials: "include",
  });

When a 401 should call a refresh endpoint that replaces the HttpOnly cookie, use API-level refresh:

const authApi = createMicroApi({
  name: "auth",
  baseUrl: "/api",
});

const auth = authApi.resource("auth", {
  refresh: authApi.post<void>("/auth/refresh", {
    authMode: "none",
  }),
});

const api = createMicroApi({
  name: "main",
  baseUrl: "/api",
  refresh: {
    fn: () => auth.refresh.fn(),
  },
});

The refresh endpoint may return 204 No Content. Parallel 401 responses share one refresh operation, then each original request retries once. Configure refresh either at the API level for cookie sessions or in tokenProvider for readable tokens, not both.

Use the default authMode: "optional" for protected cookie endpoints. authMode: "required" checks for a readable access token and therefore is not appropriate for HttpOnly cookies. Use authMode: "none" for login and refresh endpoints. Cookie-authenticated writes also need suitable CSRF protection.

Errors

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

For each request, onError is called once with the final error, including when refresh or the retried request fails. The original error is still thrown so TanStack Query retries, error state, callbacks, and error boundaries keep working normally.

Next.js SSR Hydration

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.
}

TypeScript

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();

Public API

The root package export is the public API.

Runtime exports:

  • createMicroApi
  • createTokenProvider
  • MicroApiError
  • MicroAuthRequiredError

Type exports:

  • MicroApi
  • CreateMicroApiConfig
  • MicroRequestContext
  • TokenProvider
  • TokenProviderConfig
  • RefreshTokenConfig
  • BuiltResource
  • QueryEndpoint
  • MutationEndpoint
  • QueryConfig
  • MutationConfig
  • MicroQueryKey
  • VariablesArgs
  • AuthMode
  • BodyType
  • HttpMethod
  • MaybePromise
  • PathBuilder
  • RequestMappers

Examples and Docs

Example app:

cd examples/next
npm install
npm run dev

Docs app source:

cd docs
npm install
npm run dev

AI Coding Agents

micro-rq publishes version-matched agent docs at node_modules/micro-rq/dist/docs/.

Run this command in your project root to install the local micro-rq agent skill:

npx micro-rq agents init

That command creates or updates:

.agents/
  micro-rq/
    SKILL.md
    references/
      index.md
      create-micro-api.md
      create-token-provider.md
      resources.md
      tanstack-query.md
      errors.md

Publishing

npm run release:check
npm publish

release:check runs typecheck, tests, type tests, build, and npm pack --dry-run.

Published files are limited to dist, README.md, CHANGELOG.md, and LICENSE.