Skip to content

codegen: in-project api.d.ts drags entire backend module graph into every client TS program #154

@jaybo1001

Description

@jaybo1001

Title

codegen: in-project api.d.ts drags entire backend module graph into every client TS program

Body

Problem

packages/.../convex/_generated/api.d.ts is generated as:

import type * as moduleA from "../moduleA.js";
import type * as moduleB from "../moduleB.js";
// ...one line per backend module
declare const fullApi: ApiFromModules<{
  "moduleA": typeof moduleA,
  "moduleB": typeof moduleB,
  // ...
}>;
export declare const api: FilterApi<typeof fullApi, FunctionReference<any, "public">>;
export declare const internal: FilterApi<typeof fullApi, FunctionReference<any, "internal">>;

This is codegenDynamicApiObjects in dist/esm/cli/codegen_templates/component_api.js (and the equivalent in codegen_templates/api.js for the project root).

Any client that imports api (which every Convex client must, to get end-to-end typed function references) forces TypeScript to load and check every backend module to derive the api type. import type * as does not prevent module loading for type resolution — TS still parses each source file to compute its exported types, then ApiFromModules<> and FilterApi<> walk the result via recursive conditional+mapped types.

Measured impact (real production monorepo)

A monorepo with a non-trivial backend (1,103 convex modules, 3,721 registered functions, ~503k lines of .d.ts defs):

Metric mobile-rn (Expo/RN) web-react (Vite)
Backend files pulled into client TS program 1,199 1,200
Memory used (tsc --extendedDiagnostics) 3.43 GB 3.66 GB
Strict subtype cache size 439,040 21,560
Total time 47.22 s 58.58 s

Both clients sit just under the V8 default 4 GB heap. Any growth tips them over — intermittent OOMs in CI/Vercel/EAS that do not reproduce on developer machines with --max-old-space-size=8192. Devs spend hours bisecting "what changed in the type system" when the answer is "the backend grew, the type-checker noticed."

Precedent: convex already does the right thing for components

codegenStaticApiObjects (same file) emits each component's api/internal as inlined FunctionReference<udfType, visibility, args, returns> declarations — fully self-contained, zero source-file dependencies. It uses parseValidator + validatorToType from validator_helpers.js against the analyzed function metadata.

The static path is gated on opts.staticApi and currently only triggers for components, because in-project codegen runs locally without the server-side function analysis that components have via startPush.analysis.

Proposed change

Make in-project api.d.ts codegen produce the inlined-FunctionReference shape too. The function metadata (args validator JSON, returns validator JSON, visibility, udfType) needed by validatorToType is statically derivable from the .ts source files: every registered function is export const X = (mutation|query|action|internalMutation|internalQuery|internalAction)({args: v.object({...}), returns: v...., handler: ...}). The CLI can parse each backend file's AST during codegen, extract those validator literals, and synthesize the same FunctionReference strings that the component path emits today.

Concretely:

  1. Add codegenStaticApiFromSources(modulePaths) that:
    • For each module, walk top-level export const declarations
    • Match call expressions whose callee is a registration wrapper (configurable list — convex's own + secureMutation/secureQuery from convex-helpers)
    • Extract the args and returns validator object literals; convert via validatorToType
    • Emit FunctionReference<udfType, visibility, argsType, returnsType> per leaf
    • Build the api/internal trees the same way buildApiTree does for components
  2. Default --codegen=static for new projects (gated by a flag for backward compat); existing projects opt in.

Workaround we're shipping while waiting

Local post-codegen step using the TS Compiler API: node scripts/flatten-api-dts.mjs. Walks the convex/ module graph, identifies registered functions via RegisteredQuery|Mutation|Action alias-symbol detection (with AST fallback for files where wrapper imports are broken or use convex-helpers secure*), and rewrites api.d.ts in the inlined shape. Runs after convex deploy and is enforced by a pre-commit gate that rejects unflattened api.d.ts.

Result on the same monorepo: mobile-rn drops from 1,199 → 120 backend files, 3.43 GB → 2.13 GB, 47s → 11s. web-react drops from 1,200 → 113 files, 3.66 GB → 2.36 GB, 58s → 21s. Zero new errors caused by the flatten itself; 13 errors surfaced that the dynamic shape was hiding by typing missing functions as any (real codebase bugs we wanted to find).

Happy to open the PR if there's interest in landing this upstream — the script we wrote is the prototype; a CLI-native version would be cleaner because it has access to convex's own parseValidator/validatorToType and doesn't need to spin up a TS Program just to walk types.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions