Skip to content

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.

The binary is shipped inside @use-q/api-client, so once you’ve installed the core package you can call it via your package manager:

Terminal window
pnpm exec use-q-codegen --input ./openapi.json --output ./src/api/schema.ts
Terminal window
npx use-q-codegen --input ./openapi.yaml --output ./src/api/schema.ts --base-url https://api.example.com
FlagDescription
--inputPath to an OpenAPI 3.x spec (.json or .yaml). Required.
--outputPath to write the generated schema.ts. Required.
--base-urlOptional. If provided, emitted as a BASE_URL constant alongside the schema.

YAML inputs use js-yaml under the hood, so anchors and multi-line strings are supported.

The emitter produces a single TypeScript file with three sections:

// 1. Component schemas → TS interfaces
export interface Post {
id: string;
facilityId: string;
title: string;
body: string;
createdAt: string;
}
// 2. Operation-level types (request bodies, parameters)
export interface CreatePostInput {
title: string;
body: string;
}
// 3. RouteDefinition map, ready for createApiClient
import type { Schema } from "@use-q/api-client";
export const schema = {
listPosts: {
method: "GET",
path: "/facilities/{facilityId}/posts",
pathParams: {} as { facilityId: string },
searchParams: {} as { search?: string; page?: number; limit?: number },
response: {} as { items: Post[]; total: number },
pagination: {
kind: "page-number",
pageParam: "page",
itemsField: "items",
totalField: "total",
},
},
createPost: {
method: "POST",
path: "/facilities/{facilityId}/posts",
pathParams: {} as { facilityId: string },
body: {} as CreatePostInput,
response: {} as Post,
},
// …
} as const satisfies Schema;

Route names use the OpenAPI operationId. If an operation has no operationId, the emitter falls back to methodPathSegments (e.g. GET /posts/{id}getPostsId).

$ref pointers within components/schemas are inlined as TypeScript interfaces. Composition keywords are handled like so:

OpenAPITS
allOfintersection (A & B)
oneOfdiscriminated union (A | B)
anyOfunion (A | B)
enumstring-literal union
nullable: trueT | null

The emitter inspects the response schema and infers a pagination block automatically:

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

The pageParam/cursorParam is taken from the parameter named page, pageNumber, or cursor in the operation. If none is found and pagination shape is detected, the emitter warns to stderr and skips emitting the pagination block — you can hand-add it.

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

  • Tagstags and invalidatesTags aren’t part of OpenAPI. Add them after codegen so you keep them on regenerations:

    // Hand-added after codegen
    schema.listPosts.tags = [
    { type: "post", facilityId: (p) => p.facilityId },
    ];

    …or keep the generated file pristine and compose tags in a sibling file:

    src/api/schema.tags.ts
    import { schema as base } from "./schema.generated";
    export const schema = {
    ...base,
    listPosts: {
    ...base.listPosts,
    tags: [{ type: "post", facilityId: (p) => p.facilityId }],
    },
    } as const;
  • Custom error types. Codegen emits a default unknown parsed-error type. Pair it with your own parseError in createApiClient to surface ApiProblem/RFC 7807 fields.

  • Renaming routes. If operationId is missing or ugly, rename keys in the generated file. The TS-only pathParams/response will follow.

A common setup:

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

For monorepos:

  1. Put the OpenAPI spec in a packages/api-spec package.
  2. Generate the schema into a shared packages/api-schema package.
  3. Import schema from @org/api-schema into both web and CLI consumers.

See Monorepo usage for a full walkthrough.

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

import { generate } from "@use-q/api-client-codegen";
await generate({
input: "./openapi.json",
output: "./src/api/schema.ts",
baseUrl: "https://api.example.com",
});

generate resolves once the file has been written and Prettier-formatted (using your project’s config if found).

The emitter calls Prettier with your project’s resolved config (or its defaults). To turn that off, set format: false in the programmatic API. The CLI always formats.