You have an OpenAPI document and a request for a TypeScript SDK by Friday. The two generators you tried produced a client per endpoint and a dependency tree you did not choose. Fern promises consistent SDKs across languages from one config file, and its documentation assumes you know what that file holds, which group runs, and why the command wants a login.
What you get
You will end up with a Fern project from your document, a validated generators.yml with two
groups, a script that prints what each form of fern generate runs, and tests that pin the
config. This is for you if you own an API and want a maintained generator.
Short answer
Run fern init --openapi openapi.json --organization YOUR_ORG to create fern/ with
fern.config.json and generators.yml, then fern check --local to validate the definition.
fern generate runs the default-group on Fern’s cloud and asks you to log in;
fern generate --local runs the same generators as Docker images on your machine. Name the group
every time, because the group decides which SDKs are built and where they go.
You will need
Node 22 or later, an OpenAPI 3 document, and, for local generation,
Docker. Verified 2026-10-21 against Node 22.22.0,
fern-api 5.163.1, and yaml 2.9.1. The CLI is installed from a lockfile as fern-api, so
npx fern runs the pinned release. That release writes generators.yml, which Fern’s
documentation files under its legacy SDK docs. A later release writes an
sdk-config.yml from fern init
instead, with targets in place of groups. Generation itself was not run here: fern generate
needs a Fern account and fern generate --local needs a Docker daemon, which the verification
environment did not have. The commands on this page stop where either would start.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| Fern | You want maintained generators and one generators.yml deciding languages, versions, and outputs | A login for cloud generation or Docker for local, and a pinned generator version per language that you upgrade by hand | You cannot run Docker and will not create an account, or you need a language Fern does not ship |
| OpenAPI Generator | You want the widest language list with no account, no Docker, and templates you can edit | A Java 11 runtime or its Docker image, endpoint-shaped output, and per-language templates to own once you customize | You want entity-shaped SDKs that someone else maintains |
| Speakeasy | You want a workflow.yaml in your repository that names sources and targets and pins the CLI | A CLI that attempts an upgrade to its latest release on every run unless speakeasyVersion is pinned, and a vendor workflow around your repository | You want to run the generator with no vendor service in the loop |
The three differ in where generation runs. Fern’s generators are Docker images, run on Fern’s
cloud when you log in or by your own Docker daemon with --local. OpenAPI Generator is a jar, so
a Java runtime and a command are the whole path, with no account anywhere on it. Speakeasy keeps
a workflow.yaml in your repository that names sources and targets and pins its CLI version, and
that pin defaults to latest, which upgrades the CLI underneath you.
Initialize the project from the document
fern init takes the document and an organization name and writes a fern/ directory beside
it. It needs no login.
npx fern init --openapi openapi.json --organization acme
The command printed Created new API: ./fern and wrote three files: a .gitignore, the project
config, and the generator config.
{
"organization": "acme",
"version": "5.163.1"
}
api:
specs:
- openapi: ../openapi.json
default-group: local
groups:
local:
generators:
- name: fern-typescript-sdk
output:
location: local-file-system
path: ../sdks/typescript
version: 3.107.3
cloud:
generators:
- name: fern-python-sdk
version: 5.41.3
output:
location: local-file-system
path: ../sdks/python
fern.config.json pins the CLI release the project runs, which fern init sets to the release
current on the day you run it.1 generators.yml names the spec, a default group, and one
generator: the TypeScript SDK, at a pinned version, written to a directory beside fern/. The
cloud group came from a second command, npx fern add fern-python-sdk --group cloud, which
looked up that generator’s current version and appended the group. Both versions are yours to
upgrade from then on, since the CLI changes them only when asked.
inspect.mjs reads the file the CLI reads and prints what each form of the generate command
would run.
node inspect.mjs
specs: ../openapi.json
default group: local
group local (fern generate)
fern-typescript-sdk 3.107.3 -> local-file-system ../sdks/typescript
group cloud (fern generate --group cloud)
fern-python-sdk 5.41.3 -> local-file-system ../sdks/python
Validate offline and read what Fern made of the document
fern check validates
the definition and the configuration. The --local flag keeps the work on your machine; without
it, the CLI’s own help text says, the definition is sent to Fern’s API for validation.
npx fern check --local
All checks passed
Fern reads OpenAPI into a definition format of its own, and fern write-definition writes that
reading under fern/.definition, which the .gitignore from fern init excludes.
definition.mjs runs it and prints the types and endpoints it found.
node definition.mjs
[api]: Wrote definition to fern/.definition
types: Customer, Order, OrderStatus
createOrder POST /orders -> Order
deleteOrder DELETE /orders/{orderId} -> no body
getCustomer GET /customers/{customerId} -> Customer
getOrder GET /orders/{orderId} -> Order
listCustomers GET /customers -> list<Customer>
listOrders GET /orders -> list<Order>
updateOrder PATCH /orders/{orderId} -> Order
Seven endpoints and three types, with the inline enum promoted to OrderStatus. Read this before
generating, because an operation missing here is missing from every SDK.
Choose the group, then generate
A bare fern generate runs the default group remotely. The
SDK commands page
says remote generation, the default, requires authentication, and the CLI says so as well.
npx fern generate
[api]: Using default group 'local' from generators.yml
Authentication required. Please run 'fern login' or set the FERN_TOKEN environment variable.
Both lines matter. The first names the group that would have run, which is the one thing to check
before a login makes the command real. --local takes the account out of the loop and puts Docker
in: the same page says a Docker daemon must be running on your machine, because SDK generation
runs inside a Docker container.2
npx fern generate --local
npx fern generate --group cloud --local
Run here, the first command validated the definition, named the image it was about to execute,
fernapi/fern-typescript-sdk at the pinned version, and stopped when no Docker daemon answered.
Each group’s output.path says where its SDK lands once a daemon is present: sdks/typescript
for the first command and sdks/python for the second, which leaves the first alone.
Check it worked
Five tests with the Node test runner pin the config and run the CLI twice: once to validate, once to refuse a group that does not exist.
test('a group that does not exist is refused before anything runs', () => {
const r = spawnSync('npx', ['fern', 'generate', '--group', 'nope'], { encoding: 'utf8' })
assert.equal(r.status, 1)
assert.match(r.stdout + r.stderr, /'nope' is not a valid group or alias\. Available groups: local, cloud/)
})
node --test fern.test.mjs
1..5
# tests 5
# suites 0
# pass 5
# fail 0
# cancelled 0
# skipped 0
# todo 0
# pass 5 is the line to look for. The test for the missing group asserts exit status 1 and the
message naming the available groups, so a typo in a CI script fails before any image is pulled.
When it goes wrong
Every generator in a group runs when the group runs. A default group holding a TypeScript, a
Python, and a Go generator that publish to three registries runs all three on a bare
fern generate, which is what forgetting --group costs. Keep default-group pointing at a
group that writes to the local file system, as fern init does, and name the publishing group
explicitly. Without a default group the CLI refuses and lists the choices, which the copy of the
project under nodefault/ shows.
cd nodefault && npx fern generate
[api]: No group specified. Use the --group option, or set "default-group" in generators.yml:
› local fern generate --group local
› cloud fern generate --group cloud
A group name is misspelled. The CLI stops before anything runs and lists what exists.
npx fern generate --group nope
[api]: 'nope' is not a valid group or alias. Available groups: local, cloud
The CLI that runs is not the CLI you installed. fern.config.json pins a release, and the
version policy says each project runs the release it pins. A project initialized with an earlier
installed release can therefore carry a later pin from the day it was created. fern upgrade moves
the pin on purpose. Read the file before assuming the two match.
When not to do this
Do not pick Fern if you cannot run Docker and will not hold a vendor account. Both routes to a generated SDK go through one or the other, and OpenAPI Generator produces a client from a jar with neither.3
Do not let default-group name a group that publishes. A bare fern generate runs whatever that
group holds, so a maintainer who runs it to try a change ships to a registry. Default to a group
that writes to the local file system and make publishing a group you have to name.
Do not take fern check --local passing as a generated SDK. It validates the definition and the
configuration, and fern generate --local ran the same checks here before it reached for Docker.
The SDK exists when the image has run, not before.
Do not loosen the generator versions. generators.yml takes an exact version per generator and
fern init writes one. Keep it exact, because a version that moves turns the next regeneration
into a review of someone else’s changes.
Related how-tos
Last verified
Verified 2026-10-21 against Node 22.22.0, fern-api 5.163.1, and yaml 2.9.1. Every output block is
what the command preceding it printed, run in the page’s code directory after npm ci.
fern init and fern add ran once to write the committed fern/ directory and are not re-run
by the checks, because both fetch current versions from the registry. fern generate --local was
run once to observe the Docker requirement and did not produce an SDK. Nothing on this page ran
with a Fern account, and the generator versions in generators.yml are the ones the CLI wrote on
2026-10-18. On 2026-10-21, fern init from fern-api 5.170.3, run once in a scratch directory,
wrote sdk-config.yml and no generators.yml.
Footnotes
-
Fern’s version policy says each project runs the version it pins in
fern.config.json, and that an organization policy can move the version that runs without changing the file.fern initwrites the release current on the day it runs rather than the one running it. A project initialized here with the previous CLI release carried the newer number before that release was installed anywhere on the machine. ↩︎ Back to text -
The SDK commands page says a Docker daemon must be running on your machine, as SDK generation runs inside a Docker container. The container is the generator.
generators.ymlnames it by package name and version, and the CLI resolves that pair to an image under thefernapinamespace. That image name is what it printed here in the moment before it found no daemon to talk to. ↩︎ Back to text -
The OpenAPI Generator installation page asks for a Java 11 runtime at a minimum and describes its npm package as a cross-platform wrapper around the jar. It also offers a Docker image that acts as a standalone executable. The page asks for no account at any step. ↩︎ Back to text