Manual SDK maintenance has a consistent arc: it starts as a reasonable investment, accumulates technical debt as the API evolves faster than the SDK can follow, and eventually becomes a liability. The SDK for your Python users is three versions behind the API. The TypeScript types are missing three fields that were added in the last release. The Go client's authentication helper does not handle token refresh correctly. Every one of these gaps is a support ticket, a broken integration, and a developer who writes "SDK is unmaintained" in a forum thread that other potential users will find.
The alternative is treating SDK generation as a pipeline step that runs automatically on every spec change. This post walks through what that pipeline looks like in practice, covers the tooling tradeoffs between the major SDK codegen options, and addresses the parts that people typically underestimate: versioning alignment, publishing automation, and the ongoing quality bar for generated output.
Choosing a Codegen Tool
Three tools dominate serious SDK generation from OpenAPI specs in 2025: openapi-generator (the Apache-licensed open-source project), Speakeasy, and Stainless. They differ in output quality, customization surface, and operational model.
openapi-generator supports the widest language set — TypeScript, Python, Go, Java, C#, Ruby, and more — and is entirely self-hosted. The generated output for common languages is usable but tends to be verbose: the TypeScript client generates a class per operation with explicit request/response types, which is functional but not ergonomic by the standards of hand-crafted SDKs. Pagination helpers, retry logic, and authentication middleware require custom templates or post-generation patching. It is a strong choice when breadth of language support matters more than output aesthetics, or when you need full control over the generation pipeline without a vendor dependency.
Speakeasy and Stainless are both newer and opinionated toward output quality for a narrower language set. Speakeasy's TypeScript and Python output reads closer to a hand-written SDK — idiomatic async patterns, typed error classes, built-in retry configuration. Stainless focuses heavily on TypeScript and Python with what they call "hand-crafted feel" output. Both are commercial services with per-SDK pricing, which is a reasonable tradeoff for teams that prioritize DX over cost at SDK scale.
We are not saying one of these tools is universally correct. The right choice depends on your language targets, your team's capacity to maintain custom templates, and your tolerance for vendor dependency in the build pipeline. What matters more than the tool choice is the principle: SDK generation must be automated, must be triggered by spec changes, and must produce output that is version-aligned with the API.
Pipeline Architecture: Trigger, Generate, Test, Publish
A minimal working pipeline has four stages: trigger on spec change, generate the SDK, run smoke tests, publish the package. Each stage has decisions that affect how reliable the pipeline is in practice.
The trigger is a spec change on your main branch, not a release tag. If you use release tags to trigger SDK generation, you introduce a manual step (cutting the tag) that gets skipped under time pressure. Triggering on spec changes to main means the SDKs are always generated from the current spec. The version of the generated SDK can be derived from the spec version: a patch change in the spec version produces a patch SDK version bump, a minor change produces a minor bump, a breaking change (detected by your oasdiff check) produces a major bump and requires an explicit approval step before publishing.
# .github/workflows/sdk-publish.yml
name: Generate and Publish SDKs
on:
push:
branches: [main]
paths:
- "openapi.yaml"
jobs:
generate-typescript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install openapi-generator
run: npm install -g @openapitools/openapi-generator-cli
- name: Generate TypeScript SDK
run: |
openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o sdk/typescript \
--additional-properties=npmName=@yourorg/api-client \
--additional-properties=supportsES6=true
- name: Run SDK tests
run: |
cd sdk/typescript
npm install
npm test
- name: Publish to npm
if: success()
run: |
cd sdk/typescript
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
The smoke test stage is the one most commonly skipped and most important for maintaining SDK quality. A generated SDK that fails to instantiate the client, make a basic authenticated request, or deserialize a standard response shape is worse than no SDK — it causes active integration failures. A minimal smoke test suite exercises: client instantiation with a test API key, one successful GET request against a mock server, one request that intentionally triggers a 4xx and verifies the error type is correctly surfaced. This is not a comprehensive integration test suite; it is a generation quality gate.
Version Alignment: The Detail That Breaks Things
SDK version alignment is the problem that manual SDK maintenance consistently gets wrong and that a pipeline enforces mechanically. The SDK version must unambiguously communicate which API version it targets. This does not mean the SDK version and API version must be identical — a TypeScript SDK at 2.4.1 and an API at v2 is a coherent relationship — but it means the relationship must be documented and machine-readable.
A practical pattern: embed the API version in the generated SDK's package metadata, and include a compatibility matrix in the SDK's README that the generation pipeline maintains automatically. When the API bumps from v2 to v3 (indicating breaking changes), the SDK major version bumps from 2.x to 3.x. A developer running npm install @yourorg/api-client@2 gets a client that works against the v2 API; upgrading to @yourorg/api-client@3 is an explicit migration decision.
// Generated package.json fragment (TypeScript SDK)
{
"name": "@yourorg/api-client",
"version": "2.4.1",
"description": "TypeScript client for the Yourorg API (v2)",
"keywords": ["yourorg", "api", "v2"],
"x-api-version": "2.4.1",
"x-openapi-spec-hash": "sha256:a3f91c..."
}
Including a hash of the OpenAPI spec in the package metadata is particularly useful for debugging. When a consumer files a bug saying "the client returns null for field X," you can immediately check whether they are using the SDK generated from the same spec version that production is serving, or whether they have an older generation.
Custom Templates and Output Quality
The first time you run openapi-generator against a production spec, the output is usually functional but not ergonomic. Variable names follow the schema property names exactly, including snake_case properties from a Python-centric API design showing up as snake_case in the TypeScript client. The error handling is often a generic HTTP error class rather than typed domain errors. Pagination is manual.
Custom Mustache templates — the templating layer openapi-generator exposes — let you override the output for specific constructs without forking the generator. A custom template for the TypeScript client class can add typed pagination methods. A custom template for error handling can map specific HTTP status codes to domain-specific error classes. The investment in custom templates pays off when you have multiple SDKs that share the same ergonomic patterns — the template is the shared layer that makes each generated SDK feel like a coherent product rather than a spec export.
Publishing Across Registries
Publishing TypeScript to npm, Python to PyPI, and Go to the module proxy each have different authentication and versioning mechanics. A few operational notes worth capturing:
- npm: Scoped packages (
@yourorg/api-client) are the cleanest choice for API client SDKs. Use a dedicated publish token scoped to the specific package rather than a general registry token. - PyPI: Trusted publishing via OpenID Connect (GitHub Actions to PyPI) is more secure than long-lived API tokens and is now PyPI's recommended approach for CI/CD workflows.
- Go modules: Go SDK distribution is via tagged Git commits on a repository — the module proxy reads from your Git history rather than a package registry. Version tagging must be correct:
v2.4.1for the main module, or a subdirectory structure (sdk/gotagged assdk/go/v2.4.1) for a monorepo layout.
All three should publish only when the smoke test stage passes. A flawed generation that gets published to a public registry will be cached by consumers and is significantly harder to recover from than a failed pipeline run. The immutability of npm and PyPI packages for a given version makes an incorrect publish an operationally expensive mistake.
What the Pipeline Does Not Solve
Automated SDK generation eliminates the problem of SDKs that are out of date with the spec. It does not eliminate the problem of a spec that is a poor basis for SDK generation. If your spec is missing operationId values, the generator will produce method names like getApiV2UsersUserIdOrdersGet. If your response schemas use untyped additionalProperties: true everywhere, the generated types will be mostly Record<string, unknown>. If your error responses are not consistently modeled in the spec, each error condition will surface differently in the generated client.
SDK quality is bounded by spec quality. The Spectral ruleset that enforces consistent operation IDs, typed response schemas, and consistent error modeling is not just about documentation aesthetics — it is directly upstream of the ergonomics of every SDK your pipeline generates. Investing in spec quality before investing in generation tooling is the correct ordering, not the reverse.