Generating SDKs with Fern
This guide is part of SDK Generation & Changelog Automation and covers using Fern — driven by the fern-api CLI — to turn an OpenAPI spec into idiomatic, hand-written-quality client libraries for TypeScript, Python, Java, and Go. The focus here is the fern/ configuration directory: importing a spec, wiring up per-language generators, shaping SDK output, and auto-publishing to package registries. For template-level control over a single language you may prefer OpenAPI Generator; for a managed GitHub Action flow see Speakeasy. This page does not cover building a docs portal — Fern’s docs product is separate from its SDK generators.
Fern’s value is that the generated code reads like an SDK an engineer would write by hand: real method names, typed errors, retries, and pagination helpers — not a thin transport wrapper. The trade-off is that you configure generators declaratively and run them through Fern’s CLI rather than a single local binary, and that publishing flows through Fern’s hosted generators or a Docker image rather than a one-line invocation.
Three ideas underpin everything in this guide. First, your OpenAPI document stays the single source of truth — Fern reads it directly and you never maintain a parallel API description. Second, generators are versioned, language-specific images: you pin the TypeScript generator separately from the Python one, so you can upgrade one language without churning the others. Third, output destinations are first-class — a generator does not just emit files, it knows whether those files belong on npm, PyPI, Maven, or a GitHub repo, which is what makes auto-publishing a configuration concern rather than a scripting one. Hold those three ideas in mind and the rest of generators.yml reads naturally.
Prerequisites & Environment Setup
Fern runs as a Node CLI but generates SDKs in every supported language without you installing those toolchains locally — generation happens in Fern’s hosted generators (or in Docker when run with --local). Pin the CLI so generator behaviour is reproducible across machines and CI.
# Node 18+ required; 20 LTS recommended
node --version # v20.x
# Install the CLI as a dev dependency (preferred over a global install)
npm install --save-dev [email protected]
# Verify
npx fern --version # 0.64.0
Scaffold the configuration directory at the root of your repository:
npx fern init --openapi ./openapi.yaml
This creates a fern/ directory:
fern/
fern.config.json # organization + pinned CLI version
generators.yml # which SDKs to generate and where to publish
openapi/
openapi.yaml # your imported spec (the source of truth)
Keeping the spec under fern/openapi/ rather than referencing it from elsewhere in the repo matters: Fern resolves relative $refs from its own working directory, so co-locating the spec with the configuration avoids the most common source of “unresolved reference” failures. If your spec is split across many files, keep the whole tree under fern/openapi/ and point generators.yml at the entry document.
Set the registry credentials Fern needs to publish. Never commit these — export them in your shell for local runs and store them as repository secrets in CI:
export NPM_TOKEN="npm_xxx" # npm automation token (publish scope)
export PYPI_TOKEN="pypi-xxx" # PyPI API token (project- or account-scoped)
export FERN_TOKEN="fern_xxx" # from `fern login` or the Fern dashboard
Run fern login once on your workstation to cache FERN_TOKEN; the token authorizes the hosted generators that produce non-Node SDKs. In CI you do not log in interactively — you set FERN_TOKEN as a secret and the CLI picks it up from the environment. A useful sanity check before going further is to confirm the CLI can see your organization and the generators it will pull:
npx fern generator list # shows available generator images and tags
Core Configuration
Two files drive everything. fern.config.json identifies your organization and pins the CLI version so every contributor and CI run uses the same generator semantics:
{
"organization": "acme",
"version": "0.64.0"
}
generators.yml is where you import the spec and declare generators. Each generator targets one language, pins a generator image version, and chooses an output destination. Annotate every non-obvious key inline:
# fern/generators.yml
api:
# Import an existing OpenAPI document. The spec stays the source of truth;
# no Fern Definition is required. You can also point at a directory of specs.
specs:
- openapi: openapi/openapi.yaml
overrides: openapi/overrides.yaml # optional: patch the spec without editing it
groups:
# A named group bundles generators you invoke together: `fern generate --group public`
public:
generators:
- name: fernapi/fern-typescript-node-sdk
version: 0.43.0 # pin the generator image, not just the CLI
output:
location: npm # publish target: npm | pypi | maven | local
package-name: "@acme/sdk" # the published package name
token: ${NPM_TOKEN}
config:
namespaceExport: AcmeApi # the top-level client export name
includeApiReference: true # generate a README with usage snippets
allowCustomFetcher: true # let consumers inject their own fetch impl
- name: fernapi/fern-python-sdk
version: 4.3.0
output:
location: pypi
package-name: "acme"
token: ${PYPI_TOKEN}
config:
client_class_name: AcmeClient
pydantic_config:
version: v2 # generate Pydantic v2 models
- name: fernapi/fern-java-sdk
version: 2.20.0
output:
location: maven
coordinate: com.acme:acme-java # groupId:artifactId
username: ${MAVEN_USERNAME}
password: ${MAVEN_PASSWORD}
- name: fernapi/fern-go-sdk
version: 1.5.0
output:
# Go consumes source from a git repo rather than a package registry
location: github
repository: acme/acme-go
config:
packageName: acme
A few configuration choices repay attention. The groups structure exists so you can publish different audiences from one spec — a public group for customer-facing SDKs and an internal group with extra debug logging, for instance — and invoke each with fern generate --group <name>. The package-name for each generator is the published identifier, independent of the generator image name; getting it right up front avoids a confusing first publish under the wrong name. For the Go generator, note that location: github is deliberate: the Go ecosystem consumes libraries from version-control URLs rather than a central package registry, so Fern pushes generated source to a repository and tags it instead of uploading an artifact.
The overrides key deserves a second look. It points at a partial OpenAPI document that Fern deep-merges over the source at generation time. This is the clean way to add descriptions, examples, or x-fern-* extensions to a spec you do not own — you leave the upstream file pristine and keep your customizations in a small, reviewable overlay.
Validate the configuration and the spec before generating anything:
npx fern check
fern check resolves the spec, confirms every generator image version exists, and reports unresolved $refs or invalid output destinations — catch these here rather than mid-publish. Treat it the way you treat a compiler: a green fern check is the precondition for every other command in this guide, and wiring it into a pre-commit hook keeps malformed configuration out of CI entirely.
Integration Pattern
In CI you typically run two flows. On every pull request, generate SDKs locally and run their tests to prove the spec change produces compilable code. On a tagged release (or a manual dispatch), publish. The workflow below does both, gated by the trigger.
# .github/workflows/fern-sdks.yml
name: Fern SDKs
on:
pull_request:
paths:
- "fern/**"
- "openapi.yaml"
push:
tags:
- "v*.*.*" # publish only on semver tags
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- name: Validate spec and generators
run: npx fern check
# On PRs: generate to disk and compile, but do not publish.
- name: Generate SDKs locally (PR check)
if: github.event_name == 'pull_request'
run: npx fern generate --group public --local --log-level debug
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
# On a version tag: publish to every configured registry.
- name: Generate and publish SDKs
if: startsWith(github.ref, 'refs/tags/v')
run: npx fern generate --group public --version ${GITHUB_REF_NAME#v}
env:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
The key distinction: --local writes packages to fern/generators/<lang>/ so you can inspect and test them, while a plain fern generate runs the hosted generators and pushes to the registry. The --version flag stamps the published package version; deriving it from the git tag (${GITHUB_REF_NAME#v} strips the leading v from v1.4.0) keeps the registry version and the release tag in lockstep, which is the single most effective way to prevent “version already exists” failures. For a deeper publishing walkthrough including per-registry auth quirks, see Publishing SDKs to npm and PyPI with Fern.
A few details make this workflow robust in practice. Scoping the pull_request and push triggers to the spec and fern/** paths means unrelated commits do not burn CI minutes regenerating SDKs. Running fern check before either generation branch fails fast on a malformed spec rather than deep inside a generator. And gating the publish step on startsWith(github.ref, 'refs/tags/v') ensures that only an explicit, human-cut release ever reaches a public registry — pull requests can never accidentally publish. If you want each merged spec change to open an SDK update for review rather than publish immediately, swap the tag trigger for a mode-style flow where the job commits the regenerated output to a branch and opens a pull request; the Speakeasy guide describes that review-first pattern in detail and the same shape applies here.
One operational note: the hosted generators run remotely, so the publish job needs FERN_TOKEN even when it also has registry tokens — the Fern token authorizes generation, and the npm/PyPI/Maven tokens authorize the upload. Omitting FERN_TOKEN produces an authentication error that looks like a registry problem but is not.
Advanced Options
Shaping idiomatic output with x-fern-* extensions. Fern derives method and type names from operationId, tags, and schema names, but you can override anything directly in the spec. Group operations into nested namespaces and rename a clumsy method:
# openapi.yaml
paths:
/users/{id}:
get:
operationId: getUserById
x-fern-sdk-group-name: users # client.users.*
x-fern-sdk-method-name: get # client.users.get(id)
tags: [Users]
This yields client.users.get("u_123") instead of client.getUserById(...). Keeping these overrides in the spec means every language SDK inherits the same idiomatic shape — you fix the name once and TypeScript, Python, Java, and Go all improve together. Group names compose, so x-fern-sdk-group-name: [admin, users] produces client.admin.users.get(...), which is how you model a large surface area as a navigable tree rather than a flat list of hundreds of methods.
Typed errors are another high-leverage extension. Declare which response schema a given status maps to and Fern emits a distinct exception class per error type, so consumers can catch a NotFoundError separately from a RateLimitError instead of inspecting status codes by hand:
# generators.yml (per generator config)
config:
errors:
discriminant: errorCode # the spec field that names the error
Pagination and auto-retries. Mark paginated endpoints once and Fern emits real iterators (for page in client.users.list()) in every language:
paths:
/users:
get:
operationId: listUsers
x-fern-pagination:
offset: $request.page
results: $response.data
Configure transport-level retries per generator under config (for example defaultMaxRetries: 3 on the TypeScript generator) so consumers get exponential backoff on 429/5xx without writing it themselves.
Overrides without touching the spec. When you cannot edit the upstream OpenAPI document, point overrides: at a partial YAML file. Fern deep-merges it over the spec at generation time — useful for adding x-fern-* extensions or descriptions to a vendor-owned spec while keeping the original pristine. Because the overlay is a normal YAML file under version control, every customization is reviewable in a pull request, and you can regenerate cleanly the moment the upstream spec catches up.
Auth schemes and environments. Fern reads the spec’s securitySchemes and generates the matching constructor arguments — a bearer scheme produces a token parameter, an API-key header produces a named argument. To give consumers a clean way to switch between production and staging, declare environments so the base URL is a typed choice rather than a magic string:
# generators.yml
api:
environments:
Production: https://api.acme.com
Staging: https://staging.api.acme.com
default-environment: Production
This emits new AcmeApi({ environment: Environments.Staging }), which is far harder to misuse than a free-form baseUrl string and keeps URLs out of consumer code.
Verification & Testing
Generate locally and inspect before you ever publish:
npx fern generate --group public --local
Expected tail of the output:
[typescript]: Generated to fern/generators/typescript
[python]: Generated to fern/generators/python
[java]: Generated to fern/generators/java
[go]: Generated to fern/generators/go
✓ All generators completed
Then compile and run the generated packages’ own tests to confirm the spec change is sound:
cd fern/generators/typescript && npm install && npm run build && npm test
cd ../python && pip install -e ".[dev]" && pytest
Compiling the generated output is the verification that matters most: a spec change that breaks code generation will fail the build here, before it ever reaches a registry, which is exactly why the CI workflow runs the local generation on every pull request. Treat a failed npm run build or pytest on generated code as a signal to fix the spec, not the generated files — never hand-edit generator output, because the next regeneration will overwrite it.
For publishing dry-runs, append --preview to fern generate — it builds the exact artifact and reports the version it would publish without pushing to the registry. This is the safe way to confirm the version bump and package name are correct before a release. After a real publish, confirm the package landed and the version matches what you expected:
npm view @acme/sdk version # should match your release tag
pip index versions acme # PyPI version listing
go list -m github.com/acme/acme-go@latest # confirm the Go module tag
If a consumer reports a mismatch, the registry version is authoritative — compare it against the git tag that produced it to find where the chain broke.
Troubleshooting
Generator image version X.Y.Z not found — the version under a generator in generators.yml points at an image tag that does not exist. List available versions with npx fern generator list (or check the generator’s release notes) and pin to a published tag. Do not use floating tags like latest in CI; they make builds non-reproducible.
401 Unauthorized when publishing to npm/PyPI — the registry token is missing, expired, or not exposed to the generation step. Confirm the secret is referenced in the env: block of the publish job and that the token has automation/publish scope (a read-only npm token will pass fern check but fail at publish). Tokens scoped to a single package also fail when package-name does not match.
Cannot publish version X.Y.Z: version already exists — npm and PyPI reject re-publishing an existing version. This happens when the git tag was reused or info.version did not change. Bump the version (derive it from the tag, as in the workflow above) and re-tag; never delete-and-republish a public version.
Unresolved $ref / fern check fails on a valid-looking spec — Fern resolves the spec from the fern/ directory, so relative $ref paths that work in your editor may break. Bundle the spec to a single file first and point the generator at the bundle, or move referenced files under fern/openapi/ so the relative paths resolve from Fern’s working directory. If you maintain the spec elsewhere in the repo, bundle it into fern/openapi/ as a build step rather than referencing it across directories.
Awkward method names survive every override — if client.getUserById persists after adding x-fern-sdk-method-name, the extension is almost always on the wrong node (it belongs on the operation, not the path item or a response) or the spec being generated is a stale bundle that predates the edit. Confirm the extension placement, rebundle, and re-run fern check; the override only takes effect when it is present in the document Fern actually reads.
Generated code drifts from the spec after a manual edit — someone hand-edited the SDK output and the next fern generate reverted it, or worse, a publish shipped the edited version once and the regenerated version later. Generator output is disposable; never edit it. Push the behaviour you need into the spec, an overrides overlay, or a generator config key so it survives regeneration.
FAQ
Do I need a Fern Definition or can I keep my OpenAPI spec?
You can keep your OpenAPI spec. Fern imports OpenAPI 3.x directly through the api block in generators.yml, so the spec stays the single source of truth and the Fern Definition is optional. Adopt the Fern Definition only if you need API features OpenAPI cannot express.
How does Fern avoid ugly generated names like InlineResponse200?
Fern reads operationId, tags, and schema names to build idiomatic method and type names, and you can override any remaining awkward names with x-fern-sdk-method-name and similar extensions in the spec. Naming the operations and schemas well in the spec gives clean SDKs for free.
Can Fern publish to npm and PyPI automatically?
Yes. Set output.location to npm, pypi, maven, or a Go repo in generators.yml and pass the registry tokens as environment variables, then fern generate publishes versioned packages without manual steps. Drive the version from your git tag so the registry and release stay aligned.
Is the local SDK output the same code that gets published?
Yes. fern generate --local writes the exact package that the hosted generators would publish, so you can inspect, run tests against, and commit the output before turning on publishing. Use --preview for a build-only dry run when you want the version report without writing all files.
Related
- SDK Generation & Changelog Automation — the parent overview of SDK and changelog tooling
- Publishing SDKs to npm and PyPI with Fern — registry auth and release detail
- Configuring Fern for multiple languages — per-language generator tuning
- OpenAPI Generator — template-driven alternative for fine-grained control
- Speakeasy — managed GitHub Action approach to SDK releases