Architecture: Shared Codegen Pipeline
The codegen pipeline turns one hand-written OpenAPI 3.1 spec into typed clients for Node, Python, and Go, with api-core as the never-published TypeScript hub that sdk-node and cli-node bundle inline.
What the shared codegen pipeline is#
The shared codegen pipeline is the flow that turns one hand-written OpenAPI 3.1 document, openapi/primitive-api.yaml, into every generated HTTP client in this repository: the Node SDK's fetch client, the CLI's operation manifest, the Python SDK's openapi-python-client output, and the Go SDK's ogen client. Nothing downstream hand-edits generated code; every language regenerates from the same spec.
The TypeScript half of that pipeline lives in one workspace-internal package, @primitivedotdev/api-core, which both @primitivedotdev/sdk (sdk-node) and primitive (cli-node) depend on and bundle inline.
Why api-core exists and is never published#
api-core exists so sdk-node and cli-node share one generated TypeScript client, and it is never published because packages/api-core/package.json sets "private": true (version 0.0.0). Its description states the role directly: a workspace-internal TypeScript OpenAPI surface holding the generated client, the operation manifest, and the host-aware PrimitiveApiClient, shared by the Node SDK and the CLI. Both consumers bundle it inline at build time, so the published npm tarballs for @primitivedotdev/sdk and primitive declare no dependency on api-core and no dependency on each other.
This split exists to keep sdk-node and cli-node decoupled at the source level while avoiding two copies of the generated OpenAPI client. Without api-core, either:
- sdk-node and cli-node would each generate and maintain their own copy of the TypeScript client (drift risk), or
- one package would depend on the other (coupling the CLI's release cadence to the SDK's, or vice versa).
api-core solves both by being a shared build-time-only dependency that disappears from the published artifact.
What api-core exports#
packages/api-core/src/index.ts is the single entry point, re-exporting the generated operations and types, the client primitives, the OpenAPI document and operation manifest, PrimitiveApiClient, and isMemoryJsonValue:
| Export | What it is |
|---|---|
operations, generated types | The full generated operation surface (./api/index.js, ./api/sdk.gen.js), one function per OpenAPI operationId |
createClient, Client, Config | The generated fetch client primitives (./api/client/index.js) |
openapiDocument, operationManifest | The raw OpenAPI document and the operation manifest used by CLI tooling (primitive describe, fish completion, command generation) |
PrimitiveApiClient, PrimitiveApiError, createPrimitiveApiClient, DEFAULT_API_BASE_URL | The host-aware client wrapper and its typed error; see PrimitiveApiClient |
isMemoryJsonValue | The Memories JSON value validator |
Everything else in api-core (the generation scripts, the raw generated files) is implementation detail behind this one entry point.
How the spec becomes TypeScript#
Two scripts defined in packages/api-core/package.json run in sequence: generate:openapi normalizes the spec and emits the manifest, then generate:api runs the TypeScript generator and its fixup pass.
# from packages/api-core
pnpm generate:openapi # tsx scripts/generate-openapi-artifacts.ts
pnpm generate:api # openapi-ts -f openapi-ts.config.ts && tsx scripts/fix-generated-api-imports.ts
pnpm generate # runs both, in order
generate-openapi-artifacts.ts reads the YAML spec once and produces three outputs: the normalized primitive-api.codegen.json that the TypeScript generator consumes, an embedded copy of the raw document, and the operation manifest. The normalization step and manifest shape and the generation scripts themselves are covered in depth elsewhere; this page is about how the pieces connect.
@hey-api/openapi-ts then generates the actual TypeScript client and types from the normalized JSON. fix-generated-api-imports.ts runs immediately after to repair known gaps in that generator's output: adding .js import extensions, guarding optional-body Content-Type headers, and fixing the MemoryJsonValue type. See Generated TypeScript Client Fixups.
How sdk-node and cli-node consume api-core#
Both packages depend on api-core at build time only and inline its output into their own tarballs. During development and testing they import from @primitivedotdev/api-core as an ordinary workspace dependency. At publish time, each package's bundler inlines api-core's compiled output, so npm install @primitivedotdev/sdk or npm install -g primitive gets a self-contained package with no runtime dependency on a package named api-core.
sdk-node layers one thing on top of the re-exported api-core surface: the higher-level PrimitiveClient (send/reply/forward). Those methods need the parsed ReceivedEmail shape from sdk-node's own webhook module, a dependency api-core intentionally does not carry. See What is the Primitive Node.js SDK? for that layering.
cli-node consumes api-core more directly: it drives the operation manifest to build the CLI's generic api-command surface, describe, and shell-completion helpers, and it uses PrimitiveApiClient for every generated command. See What is the Primitive CLI?.
Why one spec instead of three#
One spec keeps all three SDKs in lockstep; three hand-maintained clients drift.
| Approach | Outcome |
|---|---|
One openapi/primitive-api.yaml, three per-language generators | Every SDK's request/response shapes, error envelope, and endpoint set stay in lockstep automatically; a spec change propagates everywhere on the next make *-generate run |
| Per-language hand-maintained clients | Node, Python, and Go clients drift independently; a new field or endpoint needs three manual implementations, and mismatches surface only at runtime |
Python and Go each run their own generator against the same normalized spec (see Non-TypeScript Codegen: Go and Python Clients), but openapi/primitive-api.yaml is authored once and never duplicated.
Author and edit openapi/primitive-api.yaml directly as OpenAPI 3.1. The normalized primitive-api.codegen.json (OpenAPI 3.0.3) is a generated build artifact for the code generators only. Never edit it by hand; regenerate it via the api-core generate scripts.
Next steps#
Run the end-to-end pipeline across all three SDK languages in one pass.
OpenAPI Spec Normalization and Codegen ArtifactsSee exactly how the 3.1 spec is normalized into codegen-friendly 3.0.3 JSON.
PrimitiveApiClientConstruct the host-aware client and handle its typed PrimitiveApiError.
Non-TypeScript Codegen: Go and Python ClientsSee how the same spec generates the Go and Python SDK clients.
Was this page helpful?