Monorepo usage
In a monorepo, you usually want one schema, shared by every consumer — web app, mobile app, CLI tools, Node services. use-q is designed for this: the schema is just a .ts module, easy to publish from a workspace package.
Recommended layout
Section titled “Recommended layout”my-org/├─ apps/│ ├─ web/ # React app, uses @use-q/api-client-react│ ├─ cli/ # Node CLI, uses @use-q/api-client│ └─ worker/ # Cloudflare Worker, uses @use-q/api-client├─ packages/│ ├─ api-schema/ # The single source of truth│ ├─ api-spec/ # (optional) OpenAPI 3.x source for codegen│ └─ ui/ # Shared React components├─ pnpm-workspace.yaml└─ package.jsonpackages/api-schema/ exports schema (and any handcrafted types). Everyone else imports it.
The api-schema package
Section titled “The api-schema package”{ "name": "@my-org/api-schema", "version": "0.1.0", "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "scripts": { "codegen": "use-q-codegen --input ../api-spec/openapi.yaml --output ./src/generated.ts", "build": "tsup src/index.ts --format esm,cjs --dts", "typecheck": "tsc --noEmit" }, "dependencies": { "@use-q/api-client": "workspace:*" }}import type { Schema } from "@use-q/api-client";import { schema as generated } from "./generated";
const facility = (p: { facilityId: string }) => p.facilityId;
// Layer hand-edited tags on top of the codegen outputexport const schema = { ...generated, listPosts: { ...generated.listPosts, tags: [{ type: "post", facilityId: facility }], }, createPost: { ...generated.createPost, invalidatesTags: [{ type: "post", facilityId: facility }], },} as const satisfies Schema;
export type { Schema } from "@use-q/api-client";The whole point of this file: codegen handles the boring 90% (types, paths, params), you sprinkle tags and invalidation hand-typed. Rerunning codegen never touches your tag wiring.
Consuming from the web app
Section titled “Consuming from the web app”import { createApiClient } from "@use-q/api-client-react";import { schema } from "@my-org/api-schema";
export const api = createApiClient<typeof schema>(schema, { baseUrl: import.meta.env.VITE_API_BASE_URL, defaultHeaders: () => ({ Authorization: `Bearer ${tokenStore.get() ?? ""}`, }),});
export const { useQ, useM, useInfiniteQ, useQClient } = api;// apps/web/package.json (excerpt){ "dependencies": { "@my-org/api-schema": "workspace:*", "@use-q/api-client-react": "workspace:*", "@tanstack/react-query": "^5.0.0", "react": "^18.0.0" }}Consuming from a Node CLI
Section titled “Consuming from a Node CLI”import { createFetcher } from "@use-q/api-client";import { schema } from "@my-org/api-schema";
const fetcher = createFetcher<typeof schema>(schema, { baseUrl: process.env.API_BASE_URL!, defaultHeaders: () => ({ Authorization: `Bearer ${process.env.API_TOKEN!}`, }),});
const posts = await fetcher.fetch("listPosts", { pathParams: { facilityId: process.argv[2]! },});
console.table(posts.map(({ id, title }) => ({ id, title })));No QueryClient, no React — same schema, same types, same tag-aware fetcher (tags just don’t do anything in this context).
Consuming from a Cloudflare Worker
Section titled “Consuming from a Cloudflare Worker”import { createFetcher } from "@use-q/api-client";import { schema } from "@my-org/api-schema";
export default { async fetch(req: Request, env: Env) { const fetcher = createFetcher<typeof schema>(schema, { baseUrl: env.API_BASE_URL, fetchFn: fetch, }); const posts = await fetcher.fetch("listPosts", { pathParams: { facilityId: env.DEFAULT_FACILITY_ID }, }); return Response.json(posts); },} satisfies ExportedHandler<Env>;Codegen workflow
Section titled “Codegen workflow”-
Put the OpenAPI spec in
packages/api-spec.packages/api-spec/openapi.yaml openapi: 3.0.3info:title: My APIversion: 0.1.0paths:# ... -
Run codegen as a
prebuildscript inapi-schema:{"scripts": {"codegen": "use-q-codegen --input ../api-spec/openapi.yaml --output ./src/generated.ts","prebuild": "pnpm codegen"}} -
Wire it into CI so the generated file is always fresh:
- run: pnpm --filter @my-org/api-schema codegen- run: |if ! git diff --exit-code; thenecho "::error::Generated schema is stale. Run pnpm --filter api-schema codegen."exit 1fi -
Layer tags in
src/index.ts— never editsrc/generated.tsby hand.
See Codegen for CLI flags.
TypeScript project references
Section titled “TypeScript project references”With strict TS, you’ll want a tsconfig.json per package and a root tsconfig.json with references:
// tsconfig.json (root){ "files": [], "references": [ { "path": "packages/api-schema" }, { "path": "apps/web" }, { "path": "apps/cli" }, { "path": "apps/worker" } ]}{ "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "./src", "outDir": "./dist", "composite": true }, "include": ["src"]}{ "extends": "../../tsconfig.base.json", "references": [{ "path": "../../packages/api-schema" }], "compilerOptions": { "rootDir": "./src", "noEmit": true }, "include": ["src"]}This gives you incremental builds and accurate type-checking across the graph.
- One schema, no duplication. Resist the urge to copy slices of
schemainto individual apps. The whole point of a monorepo is that the source of truth is shared. workspace:*everywhere. Both for@my-org/api-schemaand for any@use-q/*packages you author.- CI: run codegen, then diff. This catches OpenAPI drift before merging.
- Don’t ship
@use-q/api-client-reactto non-React apps. The schema package depends only on@use-q/api-client(the framework-agnostic core).