Skip to content

Latest commit

 

History

History
335 lines (251 loc) · 13.8 KB

File metadata and controls

335 lines (251 loc) · 13.8 KB

API Codegen — Integration Guide

A self-contained, TypeScript-based code generator that parses service files (written against a typed createApi builder) and emits:

  • Per-service QueryKeys.<service>.ts
  • Merged models/QueryKeys.ts with a queryKeys object and QueryKeyType
  • Per-endpoint React Query hook files under <service>/queries/ and <service>/mutations/
  • Service index.ts barrels + top-level api/index.ts

It replaces an older C++ pipeline with a single tsx-driven script that uses ts-morph for TypeScript parsing. No build step, no compiled binaries.


Table of contents

  1. What gets generated
  2. Prerequisites
  3. Directory layout
  4. Source-file conventions
  5. Pipeline stages
  6. Validation checkers
  7. Configuration file
  8. IR schema
  9. Integration checklist (new project)
  10. Adding a new service
  11. Adding a new checker or generator
  12. Troubleshooting

What gets generated

For each discovered service (e.g. src/shared/api/auth/Auth.service.ts):

src/shared/api/
  auth/
    QueryKeys.auth.ts              ← per-service keys (auto-generated)
    queries/
      useTestQuery.auth.ts         ← one file per non-mutation endpoint
      useTestInfiniteQuery.auth.ts
      ...
      index.ts                     ← barrel
    mutations/
      useTestMutation.auth.ts
      ...
      index.ts                     ← barrel
    index.ts                       ← barrel: models + queries + mutations
  models/
    QueryKeys.ts                   ← merges every *_QUERY_KEYS; exports queryKeys + QueryKeyType
  index.ts                         ← top-level api/index.ts (auto-regenerated)

Every generated file is fully overwritten on each run — never hand-edit them.


Prerequisites

Runtime deps (in src/shared/api/)

The generated hooks import from these modules — they must exist in your target project with matching exports:

Import path (from generated file) Required exports
../../createApi HookFn<TRequest, TResponse> type
../../queryClient getQueryClient()
../../hooks useQueryWithOptions, useInfiniteQueryWithOptions, useSuspenseQueryWithOptions, useSuspenseInfiniteQueryWithOptions, useQueriesWithOptions, usePrefetchQueryWithOptions, usePrefetchInfiniteQueryWithOptions, plus useMutationEvents, useQueryEvents (re-exported via top-level api/index.ts)
../../models QueryError, QueryKeyType, queryKeys, and the typed param interfaces: UseQueryWithOptionsParams, UseInfiniteQueryWithOptionsParams, UseSuspenseQueryWithOptionsParams, UseSuspenseInfiniteQueryWithOptionsParams, UseQueriesWithOptionsParams, UsePrefetchQueryWithOptionsParams, QueryFetchParams, InfiniteQueryFetchParams, PrefetchInfiniteQueryFetchParams

Dev dependencies

// package.json → devDependencies
{
  "ts-morph": "^28.0.0",
  "tsx": "^4.21.0",
  "typescript": ">=5.0"
}

Script

// package.json → scripts
{
  "api-codegen": "tsx scripts/api-codegen/run.ts"
}

Also requires a working yarn run lint:fix (or swap the command in run.ts) — the pipeline runs it after generation.


Directory layout

Copy this folder verbatim into the target project:

scripts/api-codegen/
  run.ts                  ← entrypoint (orchestrator)
  run-codegen.sh          ← optional thin wrapper: `npx tsx run.ts`
  parse.ts                ← ts-morph parser → IR
  checkers.ts             ← validation rules
  utils.ts                ← shared string/path/IO helpers
  ir-types.ts             ← IR + CodegenConfig type definitions
  codegen.config.json     ← project-specific configuration
  generators/
    server-hooks.ts       ← emits per-endpoint hook files
    query-keys.ts         ← emits per-service QueryKeys.<svc>.ts
    merge-query-keys.ts   ← emits models/QueryKeys.ts
    update-index.ts       ← emits top-level api/index.ts

ir.json is a debug artifact (full parsed IR). It's written next to run.ts on every run — gitignore it if you don't want it checked in.


Source-file conventions

The parser relies on naming and shape — no decorators or magic comments.

File names

Pattern Meaning
**/*.service.ts or **/*Service.ts Service definition (has createApi<T>()...)
**/*.api.ts, **/models.ts Model files — exported interface / type declarations are indexed

Configurable via codegen.config.json (globs).

Service shape

Two accepted forms; both are detected:

// Classic
export const AuthService = createApi<AuthServiceAPI>()({
  endpoints: builder => ({ ... }),
});

// Chained (with injected client)
export const AuthService = createApi<AuthServiceAPI>().configure({
  client: axiosBaseQuery,
})({
  endpoints: builder => ({ ... }),
});

Hard requirements (enforced by checkers):

  1. The interface passed as createApi<T> must be declared in the same file.

  2. The endpoints lambda parameter must be named exactly builder.

  3. Each endpoint property in the interface must be typed as Promisify<TRequest, TResponse> (the parser literally regex-matches Promisify<…,…>).

  4. Endpoints must use new-style call forms only:

    • builder.X(fn) — standard
    • builder.X.hook(fn) — hook-style (lets the function use React hooks)
    • builder.X.configure({ client })(fn) — per-endpoint client override

    The legacy builder.X(fn, options) form is rejected by checkNoLegacyOptions.

Builder kinds

Recognized: query, mutation, infiniteQuery, suspenseQuery, suspenseInfiniteQuery, queries, prefetch, prefetchInfiniteQuery.

Each kind gets a different hook template (see generators/server-hooks.ts). mutation goes under mutations/; everything else under queries/.

Service-name derivation

  • The exported const next to createApi<T>() is the service constant (e.g. AuthService).
  • The service stem is derived from the file basename: Auth.service.tsauth; AuthService.tsAuth. Lowercased where used.
  • Generated artifacts derive from the stem:
    • File suffix: .<stem>.ts (e.g. useTestQuery.auth.ts)
    • Key prefix (snake-upper): TEST_QUERY_AUTH_SERVICE
    • Hook/fn prefix (pascal): testQueryAuthService

Pipeline stages

run.ts executes in order:

  1. Load configcodegen.config.json via loadCodegenConfig().
  2. Parse (ts-morph)buildIR(config) walks the matched source files and emits IR.
  3. Write IR — JSON dumped to config.irOutput (debug artifact).
  4. Run all checkers — any issue halts the pipeline with process.exit(1) before any file is written.
  5. Generate server hooksgenerators/server-hooks.ts. Deletes the existing queries/ and mutations/ directories for each service first, then rewrites from scratch.
  6. Generate per-service QueryKeys.<svc>.tsgenerators/query-keys.ts.
  7. Generate merged models/QueryKeys.tsgenerators/merge-query-keys.ts.
  8. Generate api/index.tsgenerators/update-index.ts (re-exports every direct subfolder of api/ except models and hooks).
  9. Run yarn run lint:fix — inherits stdio; a non-zero exit halts the script.

Validation checkers

Defined in checkers.ts — runs before any file is written.

Checker Rejects
checkUniqueTypes Same interface/type name declared in multiple model files
checkBuilderName endpoints: arg => … where arg isn't named builder
checkGenerics createApi<X> where X isn't defined in the same service file
checkNoLegacyOptions builder.X(fn, { … }) — forces migration to configure/hook

To add one, append to the CHECKERS tuple.


Configuration file

scripts/api-codegen/codegen.config.json:

{
  "apiRoot": "src/shared/api",
  "modelFilePatterns": ["**/*.api.ts", "**/models.ts"],
  "serviceFilePatterns": ["**/*.service.ts", "**/*Service.ts"],
  "irOutput": "scripts/api-codegen/ir.json"
}

Paths are resolved against the project root (two levels up from scripts/api-codegen/).

  • apiRoot — where both the input services live and the generated models/QueryKeys.ts and index.ts are written.
  • modelFilePatterns — globs relative to apiRoot. Each matched file's exported interfaces and types are indexed (feeds import resolution in generated hooks).
  • serviceFilePatterns — globs relative to apiRoot. Each matched file is parsed for a createApi call. Files matching both categories are treated as services only.
  • irOutput — where to write the debug IR (relative to project root).

IR schema

See ir-types.ts. Summary:

IR = {
  version: 1,
  apiRoot: string,
  modelFiles: Array<{ path, declarations: [{ name, kind: 'interface' | 'type' }] }>,
  services:   Array<Service>,
}

Service = {
  path: string,                          // project-relative, forward-slashed
  folder: string,                        // immediate parent dir (e.g. "auth")
  serviceStem: string,                   // e.g. "auth"
  serviceConstName: string | null,       // e.g. "AuthService"
  apiInterfaceName: string | null,
  apiInterfaceDefinedInFile: boolean,
  builderParamName: string | null,       // must be 'builder'
  endpoints: Array<Endpoint>,
}

Endpoint = {
  name, requestType, responseType,       // strings, exactly as written in source
  builderKind: BuilderKind | null,
  callStyle: 'standard' | 'hook' | 'configure',
  clientOverride: string | null,         // text of the client override, if any
  usesLegacyOptions: boolean,
}

requestType === 'void' switches the hook template into its void variant (different signatures, no params).


Integration checklist (new project)

  1. Copy scripts/api-codegen/ into the target repo.
  2. Install deps: yarn add -D ts-morph tsx.
  3. Add script to package.json: "api-codegen": "tsx scripts/api-codegen/run.ts".
  4. Edit codegen.config.json to point at your API root and patterns.
  5. Port createApi.ts (and the HookFn type) into <apiRoot>/createApi.ts. See this repo's src/shared/api/createApi.ts — it's ~115 lines, no external deps.
  6. Set up runtime modules under <apiRoot>/:
    • queryClient.tsexport const getQueryClient = () => <your QueryClient>
    • hooks/ → wrapper hooks (useQueryWithOptions, etc.) over @tanstack/react-query
    • models/QueryError, the Use…Params/…FetchParams parameter interfaces, plus the generated QueryKeys.ts
  7. Rewrite any legacy services to the builder.X(fn) / .hook(fn) / .configure({client})(fn) forms. Run the pipeline once — checkNoLegacyOptions and checkBuilderName will catch violations.
  8. Gitignore scripts/api-codegen/ir.json if you treat it purely as a debug artifact.
  9. Wire into husky / CI (optional) if you want codegen to run on pre-commit or on CI before tsc --noEmit.

Adding a new service

  1. Create src/shared/api/<name>/<Name>.service.ts (or <Name>Service.ts).
  2. Declare the *API interface with Promisify<Req, Res> properties.
  3. Call createApi<YourAPI>()…({ endpoints: builder => ({ … }) }) and export the result.
  4. Place shared model types in models.ts or *.api.ts in the same or any scanned folder.
  5. Run yarn api-codegen. The new folder is populated with QueryKeys.<name>.ts, queries/, mutations/, and a barrel index.ts. The merged models/QueryKeys.ts and top-level api/index.ts pick it up automatically.

Adding a new checker or generator

Checker

Append to CHECKERS in checkers.ts:

export function checkMyRule(ir: IR): CheckResult {
  const issues: string[] = [];
  // inspect ir, push human-readable messages
  return { name: 'check-my-rule', issues };
}

export const CHECKERS = [
  checkUniqueTypes,
  checkBuilderName,
  checkGenerics,
  checkNoLegacyOptions,
  checkMyRule,       // ← add
] as const;

Generator

Drop generators/my-thing.ts with a generate(ir, projectRoot) export, then call it from run.ts in the order you want. Use utils.ts helpers (writeFile, rmIfExists, toImportSpecifier, the case converters, buildDeclarationIndex) to stay consistent with existing output.


Troubleshooting

Symptom Cause Fix
check-generics: createApi<X> but interface 'X' is not defined in the file Interface lives in another file or isn't exported Move/define the interface in the same .service.ts
check-builder-name: endpoints param is 'b', expected 'builder' Destructured or renamed parameter Rename the param to builder
check-no-legacy-options Using the old builder.X(fn, { client }) shape Migrate to builder.X.configure({ client })(fn)
A generated hook imports a type that doesn't exist Response/request type head isn't an exported interface/type in a matched model file Either export it from a scanned file, or verify your modelFilePatterns covers its location
lint:fix failed Project's ESLint config can't parse the output Run yarn lint manually against a single generated file to see the real diagnostic; often a missing type export in models/
Nothing happens for a service File name doesn't match serviceFilePatterns, or createApi<T> call wasn't found Check the file suffix and the exact createApi<…>() identifier
Endpoint is skipped silently builderKind couldn't be resolved — usually a typo like builder.querys or a missing serviceConstName Confirm the builder method name and that the service is exported as a const

Inspect scripts/api-codegen/ir.json after a run to see exactly what the parser extracted — most surprises become obvious there.