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.tsnpx use-q-codegen --input ./openapi.yaml --output ./src/api/schema.ts --base-url https://api.example.comCLI flags
| Flag | Description |
|---|---|
--input, -i | Path to an OpenAPI 3.x spec (.json, .yaml, or .yml). Required. May also be passed as a bare positional argument. |
--output, -o | Path to write the generated schema.ts. If omitted, the generated source is printed to stdout. |
--base-url, -b | Optional. If provided, emitted as export const baseUrl = "…" alongside the schema. |
--help, -h | Print 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:
| OpenAPI | TS |
|---|---|
allOf | intersection (A & B) |
oneOf | union (A | B) |
anyOf | union (A | B) |
enum | literal union ("a" | "b", numbers and booleans included) |
nullable: true | T | 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 shape | Inferred pagination.kind |
|---|---|
Object with items and nextCursor (or next_cursor) properties | "cursor" |
Object with items and total properties | "page-number" |
| Array or anything else | omitted |
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
parseErrorincreateApiClientto surfaceApiProblem/RFC 7807 fields — see Error handling. -
Renaming routes. If an
operationIdis missing or ugly, rename the keys in the generated file. All the type information lives in each route'ssatisfies 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.