Regenerating SDK Code from the OpenAPI Spec
Run the root Makefile codegen targets to turn openapi/primitive-api.yaml into typed clients for the Node, Python, and Go SDKs in one pass.
Every typed method in the Node, Python, and Go SDKs is generated from one file: openapi/primitive-api.yaml. Change that spec (or json-schema/email-received-event.schema.json), regenerate, and all three SDKs pick up the change with matching types.
Prerequisites: a clone of the sdks monorepo, Node.js with pnpm installed (for the TypeScript codegen scripts), and each SDK's own toolchain (uv for Python, Go >=1.25 for Go) available on PATH.
What gets regenerated#
The pipeline runs in two stages, both driven from openapi/primitive-api.yaml:
- Normalize the spec.
packages/api-core'sgenerate-openapi-artifacts.tsreads the hand-written OpenAPI 3.1 YAML and writesopenapi/primitive-api.codegen.json, a codegen-friendly OpenAPI 3.0.3 JSON document. This is the file every language generator actually consumes. Full detail on this step lives on OpenAPI Spec Normalization and Codegen Artifacts. - Generate per-language clients. Each SDK points its own generator at
primitive-api.codegen.jsonand produces a typed client in its own idiom (afetch-based TypeScript client for Node, anopenapi-python-clientpackage for Python, anogenclient for Go). See Non-TypeScript Codegen: Go and Python Clients for the Go/Python specifics.
You never hand-edit primitive-api.codegen.json. It's a build artifact; edit openapi/primitive-api.yaml and regenerate.
- 1
Edit the OpenAPI spec#
Make your change in
openapi/primitive-api.yaml. Author it as OpenAPI 3.1, that's the single source of truth for every downstream client.# openapi/primitive-api.yaml paths: /v1/widgets: post: operationId: createWidget summary: Create a widget # ...If the change is to the inbound webhook shape instead, edit
json-schema/email-received-event.schema.json, see Webhook Schema Codegen for that half of the pipeline. - 2
Regenerate every SDK from the repo root#
Run the root
Makefiletargets. This is the recommended path for any change that must ship in a single commit:cd sdks make node-generate python-generate go-generateEach target wraps that language's native generate script, so it's equivalent to (but easier to keep in sync than) running them individually.
TipIterating on a single language locally before your final commit? Run that language's native script directly instead of all three, for example
pnpm --dir sdk-node generate. Just make sure the final commit that changes the OpenAPI spec regenerates all three languages before you open a PR, reviewers and CI expect the generated output and the spec to move together. - 3
Verify the regenerated output#
Confirm the codegen artifact updated and each SDK's generated client compiles/typechecks:
git status openapi/primitive-api.codegen.json make node-check python-check go-checkExpect
openapi/primitive-api.codegen.jsonto show as modified (or untouched, if your spec edit didn't change the normalized shape), and each*-checktarget to pass. A failingnode-checkafter a spec edit usually means a new required field broke an existing hand-written caller, fix the call site, not the generated code. - 4
Commit the spec change and the generated files together#
git add openapi/primitive-api.yaml openapi/primitive-api.codegen.json \ packages/api-core/src/openapi/ sdk-node/src/api/ \ sdk-python/src/primitive/api/ sdk-go/api/ git commit -m "Add POST /v1/widgets and regenerate SDK clients"Commit the spec edit and every regenerated file in the same commit (or the same PR) so the repo never has an SDK whose generated code disagrees with the spec it was built from.
What each generator does under the hood#
Each language runs its own generator against openapi/primitive-api.codegen.json, then applies language-specific fixups to the output.
packages/api-core owns the TypeScript generation. Its generate script runs both stages:
// packages/api-core/package.json
{
"scripts": {
"generate:openapi": "tsx scripts/generate-openapi-artifacts.ts",
"generate:api": "openapi-ts -f openapi-ts.config.ts && tsx scripts/fix-generated-api-imports.ts",
"generate": "pnpm generate:openapi && pnpm generate:api"
}
}
generate-openapi-artifacts.ts reads openapi/primitive-api.yaml, writes the normalized primitive-api.codegen.json, and also emits two TypeScript files consumed elsewhere in the repo: src/openapi/openapi.generated.ts (the raw OpenAPI document as a JS constant, for tooling like primitive describe) and src/openapi/operations.generated.ts (the operation manifest, one entry per operation with its command name, method, path, parameters, and inlined request/response JSON Schemas).
generate:api then runs @hey-api/openapi-ts against the normalized spec to produce the actual fetch client, followed by fix-generated-api-imports.ts, a post-processing pass that:
- adds
.jsextensions to every relative import (required for the package's ESM output) - guards the
Content-Type: application/jsonheader on optional-body operations so it's only sent when a body is actually present - repairs the generated
MemoryJsonValuetype, which@hey-api/openapi-tscurrently widens tounknownon the OpenAPI 3.1type: "null"branch
Full detail on that fixup pass lives on Generated TypeScript Client Fixups. Both sdk-node and cli-node bundle @primitivedotdev/api-core's output inline; it is never published as its own package. See What is API Core?.
sdk-python/scripts/generate_api_client.py drives openapi-python-client against the same primitive-api.codegen.json:
# sdk-python/scripts/generate_api_client.py
subprocess.run(
[
"openapi-python-client", "generate",
"--meta", "none",
"--config", str(CONFIG_PATH),
"--path", str(SPEC_PATH), # openapi/primitive-api.codegen.json
"--output-path", str(output_path),
],
check=True,
cwd=SDK_ROOT,
)
It generates into a temp directory, then copies api/, client.py, errors.py, models/, and types.py into sdk-python/src/primitive/api/, replacing whatever was there.
Three post-processing passes run afterward:
dedupe_importsremoves duplicate module-levelfrom X import ...lines thatopenapi-python-client0.28.3 occasionally emits.guard_optional_body_content_typere-indents theContent-Typeheader assignment so it only fires when an optional body is actually sent.use_bytes_for_file_responsesfixes binary-response handling to readresponse.contentinstead ofresponse.text.
Run it directly with:
cd sdk-python
uv sync --dev
uv run python scripts/generate_schema_module.py
uv run python scripts/generate_models.py
uv run python scripts/generate_api_client.py
Or via the root Makefile target, which wraps all three scripts: make python-generate.
The Go SDK generates its sdk-go/api package with ogen from the normalized codegen spec. Run it with:
make go-generate
See Non-TypeScript Codegen: Go and Python Clients for the Go-specific generator config and fixups.
Verify the full pipeline#
Verify a regeneration by running each language's check target (typecheck, lint, and build, not just tests) before you commit:
make node-check
make python-check
make go-check
Or run everything the CI runs in one shot:
make check
Never hand-edit anything under packages/api-core/src/openapi/, sdk-node/src/api/, sdk-python/src/primitive/api/, or sdk-go/api/. Every file in those trees is regenerated on the next make *-generate run and any manual edit is silently overwritten. If the generated output is wrong, fix the generator script or the OpenAPI spec, not the output.
Next steps#
See exactly how the 3.1 YAML becomes the 3.0.3 codegen JSON and what the operation manifest contains.
Architecture: Shared Codegen PipelineUnderstand why api-core is never published and how sdk-node/cli-node bundle it inline.
Codegen TroubleshootingDiagnose schema drift, stale generated imports, and Content-Type mismatches after a regeneration.
Non-TypeScript Codegen: Go and Python ClientsDig into the Go ogen client and the Python openapi-python-client generator and their fixups.
Was this page helpful?