query-routing
v2.2.7
Published
A CLI tool that generates a complete API structure for React or Vue 3 with TanStack Query, Axios interceptors, and full TypeScript support. Detects your framework, then scaffolds typed query, mutation, infinite-query, lazy-query and cache-control hooks. Z
Downloads
243
Maintainers
Readme
query-routing
A zero-configuration CLI that generates a production-ready, strongly-typed API layer for your React or Vue 3 application.
Stop writing boilerplate. query-routing scaffolds a complete architectural foundation for your API calls, combining Axios, TanStack Query and TypeScript into a scalable, modular structure — and it detects which framework you are on before it writes a single file.
✨ Features
- ⚡️ Instant Scaffolding: A full
api/directory in seconds. - ⚛️ / 💚 React and Vue 3: Detects your framework from
package.jsonand generates the matching hooks —@tanstack/react-queryor@tanstack/vue-query. - 📄 OpenAPI/Swagger Integration: Detects your spec (
openapi.yaml,openapi.yml,openapi.json) and auto-generates routes and types. - 🧠 Intelligent Detection: Finds your
src/folder and places files accordingly. - 📦 Dependency Management: Checks for the packages your framework needs and offers to install them.
- 🛡️ Strong Typing: Every hook derives its payload and response types from one route contract. Endpoint names autocomplete; wrong ones fail to compile.
- 🔌 Interceptors Ready: Pre-configured Axios instance with request/response interceptors.
- 🪝 Five Hooks, Not One: query, mutation, infinite query, lazy query and typed cache control.
🚀 Quick Start
No global install needed. Run it inside your existing project with whichever package manager you use:
npx query-routing
# pnpm
pnpm dlx query-routing
# yarn
yarn dlx query-routing
# bun
bunx query-routingFramework detection
The CLI reads your package.json and picks the target automatically:
| What it finds | What it generates |
| ----------------------- | ----------------- |
| @tanstack/react-query | React hooks |
| @tanstack/vue-query | Vue 3 composables |
| react (and no vue) | React hooks |
| vue (and no react) | Vue 3 composables |
| both, or neither | it asks you |
The TanStack adapter wins over the framework itself, because a repo can legitimately carry both react and vue while querying with only one of them.
🔥 For Super Fast Generation (OpenAPI)
To auto-generate types and routes from your backend:
- Name your spec file exactly one of:
openapi.yamlopenapi.ymlopenapi.json
- Place it in the root of your project.
- Run the command!
What happens next?
- The CLI detects your framework and checks
package.jsonfor dependencies. - It asks to install missing packages (Axios, TanStack Query) if needed.
- It detects your
openapifile and parses your endpoints. - It generates a structured
apifolder in your project root orsrc/api(ifsrcexists).
Command-line options
Everything is interactive by default. The flags exist for CI, for scripted regeneration, and for the cases where you want to override a decision.
| Flag | What it does |
| ---------------------- | ------------------------------------------------------------------------- |
| -y, --yes | Accept every prompt. Required for non-interactive use. |
| --framework <name> | Force react or vue instead of detecting from package.json. |
| --strict | Fail the run if any $ref cannot be resolved, instead of emitting any. |
| --force | Overwrite developer-owned files too |
| --no-unwrap-envelope | Keep a response envelope your spec describes . |
| --no-openapi | Ignore any spec that is present and scaffold the starter template only. |
| --no-install | Never install missing dependencies; warn and carry on. |
| -h, --help | Print usage. |
# Regenerate types in CI, and fail the build if the spec has holes in it
npx query-routing --yes --strict📂 The Generated Structure
Identical for both frameworks — only the contents of hook/ differ:
src/api/
├── 📂 axios-config/
│ └── interceptor.ts # Centralized Axios instance & error handling
├── 📂 enum/
│ └── api-path.ts # String constants for API endpoints
├── 📂 hook/
│ ├── hook.ts # The five hooks + the public re-exports
│ ├── 📂 core/
│ │ ├── keys.ts # Hierarchical cache-key factory
│ │ ├── request.ts # Framework-free apiRequest + error helpers
│ │ └── locale.ts # Optional per-locale cache scoping
│ └── 📂 types/
│ └── hook.types.ts # Options & return types, derived from your routes
├── 📂 req/
│ └── req.types.ts # Request payload interfaces
├── 📂 res/
│ └── res.types.ts # Response payload interfaces
├── 📂 types/
│ ├── api.types.ts # Generic Response Wrappers (IAxiosData)
│ └── route.type.ts # The contract definitions for your routes
└── api-route.ts # The runtime implementation of the routesEverything is plain TypeScript in your repo. Edit it, rename it, delete what you do not use — there is no runtime dependency on this package after generation.
🛠 The Hooks
All five import from api/hook/hook.
1. useQueryWithAxios — read and cache
React
const { data, isPending, error } = useQueryWithAxios("auth", "getUserInfo");
// narrow the cached value; the result stays fully typed
const { data: names } = useQueryWithAxios("auth", "getUserInfo", undefined, {
select: (res) => res.data.map((user) => user.name),
});Vue 3 — the payload may be a plain value, a ref, a reactive object or a getter. Whatever you pass is unwrapped for both the cache key and the request, so the query refetches by itself when a filter changes.
const filters = reactive({ pageNumber: 1, pageSize: 10 });
const { data, isPending } = useQueryWithAxios("posts", "getPosts", filters);Options beyond TanStack's own:
localeAware(defaulttrue) — scope the cache entry per locale.keepPreviousData(defaultfalse) — hold the previous rows while the next page loads. Off by default because holding data keepsstatuson"success", which makesisPendingfalse forever and hides any skeleton gated on it.
2. useMutationWithAxios — write
const { mutateAsync: createUser, isPending } = useMutationWithAxios(
"auth",
"createUser",
{ invalidates: ["auth", ["posts", "getPosts"]] },
);invalidates takes a route name for a whole group, or a [route, method] tuple for one endpoint. The screens showing that data refresh themselves — no caller has to remember to refetch().
3. useInfiniteQueryWithAxios — paginated lists
Only endpoints that actually page are accepted: the payload must extend IPaginationPayload and the response must wrap an array. Anything else is not even offered by autocomplete.
React
const { items, total, isEmpty, sentinelRef, isFetchingNextPage } =
useInfiniteQueryWithAxios("posts", "getPosts", {
filters: { authorId },
pageSize: 20,
});
return (
<>
<ul>
{items.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<div ref={sentinelRef} />
</>
);Vue 3
const { items, isEmpty, loadMore, onInfiniteScroll, sentinelRef } =
useInfiniteQueryWithAxios("posts", "getPosts", {
filters: () => ({ authorId: authorId.value }),
pageSize: 20,
});<!-- Quasar -->
<q-infinite-scroll @load="onInfiniteScroll">
<li v-for="post in items" :key="post.id" v-text="post.title" />
</q-infinite-scroll>
<!-- anything else -->
<li v-for="post in items" :key="post.id" v-text="post.title" />
<div ref="sentinelRef" />Returned on top of TanStack's own fields:
| Field | What it is |
| ------------------ | ---------------------------------------------------------------- |
| items | every page flattened into one typed list |
| total | server's count when present, else the number of loaded rows |
| isEmpty | true only once loading finished and there is genuinely nothing |
| loadMore | fetch the next page, ignored while one is in flight |
| sentinelRef | attach to a bottom element to auto-load on scroll |
| onInfiniteScroll | Vue only — drop-in @load handler for Quasar |
The default getNextPageParam stops on the server's count when present, and otherwise falls back to "a short page means the last page". Override it for endpoints that report neither.
4. useLazyQueryWithAxios — fetch on demand, still cached
For data fetched in response to a user action: opening a dropdown, clicking "show result".
const {
fetch: loadUsers,
isFetching,
data,
} = useLazyQueryWithAxios("auth", "getUserInfo");
const users = await loadUsers();Doing this with a mutation works, but a mutation is uncached and un-deduplicated by design, so opening the same dropdown five times fires five identical GETs. Here the second call is free while the entry is fresh. Use refresh() to bypass the cache.
5. useApiCache — typed cache control
Nothing outside api/hook/ has to hand-build a query key.
const cache = useApiCache();
await cache.invalidate("posts"); // the whole group
await cache.invalidate("posts", "getPosts"); // one endpoint, every page
await cache.prefetch("posts", "getPosts"); // warm before navigating
cache.setData("auth", "getUserInfo", undefined, (previous) => previous);
cache.clear(); // the one call for logout()Also available: refresh, remove, cancel, getData, and queryClient as an escape hatch.
Bonus: calling the API outside a component
apiRequest is framework-free — no React, no Vue — so it works in router guards, Pinia/Redux actions, event handlers and tests, with the same types and the same validation the hooks use.
import { apiRequest, getApiErrorMessage } from "@/api/hook/hook";
const users = await apiRequest("auth", "getUserInfo");getApiErrorMessage(error), getApiErrorStatus(error) and isApiError(error) ship alongside it.
🌍 Optional: per-locale caching
A localized API returns different rows for ar and en-US, so the same request in two languages is really two different resources. Register a resolver once and every cache key picks up the locale — switching language then refetches on its own.
React
import i18n from "i18next";
import { setApiLocaleResolver } from "./api/hook/hook";
setApiLocaleResolver(() => i18n.language);Vue 3
import i18n from "@/i18n";
import { setApiLocaleResolver } from "@/api/hook/hook";
setApiLocaleResolver(() => i18n.global.locale.value);Skip this entirely and nothing changes: the default resolver returns null and the locale segment of every key is simply null. Opt out per query with localeAware: false.
🧩 Workflow: Adding a New Route
- Define types — request params in
req/req.types.ts, response shape inres/res.types.ts. - Define the contract — map the method name to those types in
types/route.type.ts. - Implement — add the Axios call in
api-route.tsusingApiPathconstants.
The hooks need no registration step; they read the contract.
// 1. types/route.type.ts
export interface IGetPostsPayload extends IPaginationPayload {
authorId: number;
}
export interface IPostRoute {
getPosts: (payload: IGetPostsPayload) => IResponse<IPost[]>;
}
// 2. api-route.ts
export const ApiRoute: IAxiosRoute = {
posts: {
getPosts: (payload) => api.get(ApiPath.POSTS, { params: payload }),
},
};Extending IPaginationPayload is what makes getPosts eligible for useInfiniteQueryWithAxios.
⚙️ Configuration
Interceptors & Auth
src/api/axios-config/interceptor.ts is where you inject tokens:
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});Base URL
The generated interceptor ships with a placeholder base URL. Point it at your own API — reading it from the environment is the usual choice:
// Vite
const baseUrl = import.meta.env.VITE_API_URL;
// Create React App / Node
const baseUrl = process.env.REACT_APP_API_URL;When you generate from an OpenAPI spec with servers entries, the CLI offers to use one of those instead.
Retries
Read hooks retry transient failures only — 408, 425, 429, 500, 502, 503, 504 and network/timeout errors — and give up after 2 attempts. TanStack's default would also retry 401s and 404s, which can never succeed and only delay the failure. Adjust RETRYABLE_STATUS and MAX_RETRIES in hook/core/request.ts.
⬆️ Upgrading from 1.x
Version 2 is a breaking change to the cache-key format.
| 1.x | 2.x |
| ------------------------------------------- | ---------------------------------------------- |
| ["posts-getPosts", payload] | ["posts", "getPosts", kind, locale, payload] |
| useQueryWithAxios, useMutationWithAxios | those two plus infinite, lazy and cache hooks |
| api/hook/hook.ts only | api/hook/ with core/ and types/ |
| React only | React and Vue 3 |
What this buys you is partial invalidation: with a tuple, any prefix is a valid filter, so ["posts"] drops a whole group and ["posts", "getPosts"] drops one endpoint across every locale and page. A flat string key can never express that.
To upgrade: back up any edits you made inside api/hook/, delete the folder, and re-run npx query-routing. Call sites of useQueryWithAxios and useMutationWithAxios keep working unchanged; only hand-written queryKey arrays and direct queryClient calls need updating to go through useApiCache.
⚖️ License
MIT License
Copyright (c) 2025 Abdelhadi Alkayal
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
💌 Feedback & Support
Questions, support or suggestions: [email protected]
