---
title: "Architecture: Shared Codegen Pipeline"
canonical: "https://test.abhinandan.one/codegen-architecture"
markdown_url: "https://test.abhinandan.one/codegen-architecture.md"
publisher: "Primitive SDKs"
kind: "concept"
content_type: "reference"
category: "API Core / OpenAPI Generation"
description: "One OpenAPI 3.1 spec at openapi/primitive-api.yaml feeds api-core's TypeScript generator, then Node, Python, and Go each generate their own typed client from it."
keywords: ["api-core", "@primitivedotdev/api-core", "primitive-api.yaml", "primitive-api.codegen.json", "PrimitiveApiClient", "operation manifest"]
last_modified: "2026-08-11T18:55:04.296301+00:00"
published_at: "2026-08-11T18:55:04.116589+00:00"
source_files:
  - "packages/api-core/package.json"
  - "packages/api-core/src/index.ts"
  - "openapi/primitive-api.yaml"
sections:
  - {anchor: "what-the-shared-codegen-pipeline-is", title: "What the shared codegen pipeline is"}
  - {anchor: "why-api-core-exists-and-is-never-published", title: "Why api-core exists and is never published"}
  - {anchor: "what-api-core-exports", title: "What api-core exports"}
  - {anchor: "how-the-spec-becomes-typescript", title: "How the spec becomes TypeScript"}
  - {anchor: "how-sdk-node-and-cli-node-consume-api-core", title: "How sdk-node and cli-node consume api-core"}
  - {anchor: "why-one-spec-instead-of-three", title: "Why one spec instead of three"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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](https://test.abhinandan.one/operation-manifest.md) 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](https://test.abhinandan.one/primitive-api-client.md) |
| `isMemoryJsonValue` | The [Memories JSON value validator](https://test.abhinandan.one/memory-json-value-helper.md) |

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.

```bash
# 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
```

```mermaid
flowchart LR
  A["openapi/primitive-api.yaml\n(OpenAPI 3.1, hand-written)"] --> B["generate-openapi-artifacts.ts"]
  B --> C["primitive-api.codegen.json\n(OpenAPI 3.0.3, normalized)"]
  B --> D["openapi.generated.ts\n(embedded document constant)"]
  B --> E["operations.generated.ts\n(operation manifest)"]
  C --> F["@hey-api/openapi-ts"]
  F --> G["generated TS client + types"]
  G --> H["fix-generated-api-imports.ts"]
  H --> I["api-core src/api/*"]
  I --> J["sdk-node (bundled inline)"]
  I --> K["cli-node (bundled inline)"]
```

`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](https://test.abhinandan.one/openapi-spec-normalization.md) and the [generation scripts themselves](https://test.abhinandan.one/codegen-workflow/api-core-generation-scripts.md) 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](https://test.abhinandan.one/typescript-client-fixups.md).

## 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?](https://test.abhinandan.one/node-sdk-overview.md) for that layering.

cli-node consumes api-core more directly: it drives the [operation manifest](https://test.abhinandan.one/operation-manifest.md) 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?](https://test.abhinandan.one/cli-overview.md).

## 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](https://test.abhinandan.one/non-typescript-codegen.md)), but `openapi/primitive-api.yaml` is authored once and never duplicated.

> **Note:** 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.
