use-q

Command Palette

Search for a command to run...

Codegen

Generate a typed RouteDefinition map from an OpenAPI 3.x spec with use-q-codegen.

use-q-codegen turns an OpenAPI 3.x document (JSON or YAML) into a fully-typed RouteDefinition map you can drop straight into createApiClient or createFetcher.

Running the CLI

The binary ships with the @use-q/api-client-codegen package, so install that (typically as a dev dependency) and call it via your package manager:

pnpm add -D @use-q/api-client-codegen
pnpm exec use-q-codegen --input ./openapi.json --output ./src/api/schema.ts
npx use-q-codegen --input ./openapi.yaml --output ./src/api/schema.ts --base-url https://api.example.com

CLI flags

FlagDescription
--input, -iPath to an OpenAPI 3.x spec (.json, .yaml, or .yml). Required. May also be passed as a bare positional argument.
--output, -oPath to write the generated schema.ts. If omitted, the generated source is printed to stdout.
--base-url, -bOptional. If provided, emitted as export const baseUrl = "…" alongside the schema.
--help, -hPrint usage and exit.

YAML inputs use js-yaml under the hood, so anchors and multi-line strings are supported. Non-3.x specs are rejected with an error.

What gets generated

The emitter produces a single TypeScript file with three sections:

// =============================================================================
// AUTO-GENERATED by @use-q/api-client-codegen — DO NOT EDIT BY HAND.
// =============================================================================
import type { RouteDefinition } from "@use-q/api-client";
 
// 1. Optional base URL (only with --base-url)
export const baseUrl = "https://api.example.com";
 
// 2. components/schemas → exported type aliases
export type Post = {
  id: string;
  facilityId: string;
  title: string;
  body: string;
  createdAt: string;
};
 
// 3. RouteDefinition map, ready for createApiClient
export const schema = {
  listPosts: {
    method: "GET",
    path: "/facilities/{facilityId}/posts",
    tags: ["posts"] as const,
    pagination: { kind: "page-number", pageParam: "page" } as const,
  } satisfies RouteDefinition<
    { facilityId: string },
    { search?: string; page?: number; limit?: number },
    never,
    { items: Array<Post>; total: number }
  >,
  createPost: {
    method: "POST",
    path: "/facilities/{facilityId}/posts",
  } satisfies RouteDefinition<
    { facilityId: string },
    Record<string, never>,
    { title: string; body: string },
    Post
  >,
  // …
} as const satisfies Record<string, RouteDefinition<any, any, any, any>>;
 
export type Schema = typeof schema;

Per-route types (path params, search params, body, response) are carried entirely by the satisfies RouteDefinition<TParams, TSearch, TBody, TResponse> clause — there are no extra runtime fields. Operation-level tags from the spec are emitted as static string arrays.

Route keys use the OpenAPI operationId. If an operation has no operationId, the emitter falls back to the literal "METHOD path" string (e.g. "GET /posts/{id}") — both forms work as schema keys, operationId is just more ergonomic in user code.

$ref resolution

$ref pointers into components/schemas are emitted by name — each component becomes an exported type alias, and references use that name. Composition keywords are handled like so:

OpenAPITS
allOfintersection (A & B)
oneOfunion (A | B)
anyOfunion (A | B)
enumliteral union ("a" | "b", numbers and booleans included)
nullable: trueT | null

Pagination detection

The emitter inspects the operation's 200 JSON response schema (following one level of $ref) and infers a pagination block automatically:

Response shapeInferred pagination.kind
Object with items and nextCursor (or next_cursor) properties"cursor"
Object with items and total properties"page-number"
Array or anything elseomitted

The pageParam is taken from the operation's query parameters: for cursor pagination, the first query parameter whose name contains cursor (case-insensitive); for page-number pagination, a query parameter named page, pageNumber, or page_number (case-insensitive). If the response shape matches but no such parameter exists, the pagination block is simply omitted — you can hand-add it.

If your API uses an unusual pagination convention, add or adjust the pagination block by hand — { kind: "page-number", pageParam } also accepts itemsKey/totalKey, and { kind: "cursor", pageParam } accepts cursorKey/itemsKey to point at nonstandard response fields. The schema is just a plain TS module.

When to hand-edit

Codegen handles ~90% of the boilerplate, but a few things should be hand-tuned:

  • Tags — codegen only copies the spec's static operation tags. Richer tags — object tags like { type: "post", id } or dynamic tag functions — aren't part of OpenAPI, so compose them in a sibling file to keep the generated file pristine across regenerations:

    // src/api/schema.tags.ts
    import { schema as base } from "./schema.generated";
     
    export const schema = {
      ...base,
      getPost: {
        ...base.getPost,
        tags: ({ response }) => [{ type: "post", id: response.id }],
      },
      createPost: {
        ...base.createPost,
        invalidatesTags: ["posts"],
      },
    } as const;
  • Custom error types. Codegen doesn't emit anything about error responses. Pair the schema with your own parseError in createApiClient to surface ApiProblem/RFC 7807 fields — see Error handling.

  • Renaming routes. If an operationId is missing or ugly, rename the keys in the generated file. All the type information lives in each route's satisfies RouteDefinition<…> clause, so it moves with the key.

Running as part of your build

A common setup:

// package.json
{
  "scripts": {
    "codegen": "use-q-codegen --input ./openapi.json --output ./src/api/schema.generated.ts",
    "prebuild": "pnpm codegen"
  }
}

For monorepos:

Put the OpenAPI spec in a shared package

Keep the spec in a packages/api-spec package so it's versioned alongside your code.

Generate the schema into its own package

Point --output at a shared packages/api-schema package.

Import the schema from consumers

Import schema from @org/api-schema in both web and CLI consumers.

See Monorepo usage for a full walkthrough.

Programmatic API

For build scripts or codemods, you can call the generator directly:

import { generate } from "@use-q/api-client-codegen";
 
const source = await generate({
  input: "./openapi.json",
  output: "./src/api/schema.ts", // optional — omit to skip writing
  baseUrl: "https://api.example.com", // optional
});

generate parses the spec, renders the schema module, writes it to output if provided (creating parent directories as needed), and resolves with the generated source string either way.

Formatting

The emitter runs the output through Prettier using your project's resolved config (or Prettier's defaults if none is found). Both the CLI and the programmatic API always format.