In Depth: Speakeasy vs Fern
Nolan Sullivan
January 16, 2024
Speakeasy (opens in a new tab) and Fern (opens in a new tab) both offer free and paid services that API developers use to create SDKs (client libraries) and automate their publication to package managers, but how do they differ? Here's the short answer:
- Fern is an SDK generation tool designed for the Fern domain-specific language (DSL). It creates SDKs in 4 languages and API reference documentation
- Speakeasy is a complete platform for building & exposing enterprise APIs. It is OpenAPI-native and supports SDK generation in 9 languages, as well as Terraform and documentation.
How is Speakeasy different?
1. We're everything you need in one
We've built a platform that does more than merely generate SDKs. You could use Fern for SDKs, Stoplight for documentation, Spectral for linting, and handroll your Terraform provider, or you could use Speakeasy for everything. One platform, one team, all your API needs handled.
2. We're built for OpenAPI
Speakeasy is designed to be OpenAPI-native. We don't believe the world needs another standard for describing APIs. OpenAPI has its flaws, but it's the established standard, and we're committed to making it better. That means that Speakeasy is interoperable with the rest of the API tooling ecosystem. Mix and match us with your other favorite tools, and we'll play nice.
As mentioned, Fern is built on top of a DSL (domain-specific language) (opens in a new tab). However, they do provide an OpenAPI importer. If you do choose to use Fern over Speakeasy, use your OpenAPI schema as the source of truth (ignore the DSL). Using OpenAPI instead of Fern's special format means you won't be locked into their service. You will also be able to continue using your schema with other OpenAPI-compatible tools.
3. We ship fast
Fern's initial GitHub commit was in April 2022 (opens in a new tab). Speakeasy's was in September 2022 (opens in a new tab). Since that time, Speakeasy has released support for nine languages, while Fern has released four.
4. Our SDKs work where you need them
Speakeasy SDKs are designed to work in any environment. We support the latest versions of the languages we target, and we're committed to keeping them up to date. Our TypeScript SDKs can be bundled for the browser and many other JavaScript environments, while Fern's are Node.js only.
We get high-quality products in the hands of our users fast.
Comparing Speakeasy and Fern
Generation Targets
Everyone has that one odd language that is critically important to their business and seemingly to nobody else's. That's why we're committed to supporting the long tail. In our first year, we've made a dent, but we've got further to go. See a language that you need that we don't support? Let us know (opens in a new tab)!
Language | Speakeasy | Fern |
---|---|---|
Go | ✅ | ✅ |
Python | ✅ | ✅ |
Typescript | ✅ | ✅ |
Java | ✅ | ✅ |
API Documentation | ✅ | ✅ |
Terraform provider | ✅ | ❌ |
C# | ✅ | ❌ |
PHP | ✅ | ❌ |
Ruby | ✅ | ❌ |
Swift | ✅ | ❌ |
Unity | ✅ | ❌ |
SDK Features
Regarding the features supported in the SDK, there are two key differences:
- Fern lacks native support for some of the more advanced enterprise features supported by Speakeasy. Features like pagination and OAuth are left up to the customer to implement via custom code.
- Fern offers customizations to the names used in the SDK but not to the fundamental structure of the SDK. In addition to names, Speakeasy allows you to customize things like the directory structure and how parameters are passed into functions.
Feature | Speakeasy | Fern |
---|---|---|
Union types | ✅ | ✅ |
Server side events | ✅ | ✅ |
Retries | ✅ | ✅ |
Webhooks | ✅ | ✅ |
Async support | ✅ | ✅ |
Streaming uploads | ✅ | ❌ |
OAuth 2.0 | ✅ | ❌ |
Pagination | ✅ | ❌ |
Custom SDK Naming | ✅ | ✅ |
Customize SDK Structure | ✅ | ❌ |
Platform Features
In terms of the platform, the major differences are:
- Fern is solely focused on the generation of artifacts. Speakeasy has a deeper platform that supports the management of API creation via the CLI's validation.
- Speakeasy offers a web interface for managing & monitoring the creation of your SDKs.
Feature | Speakeasy | Fern |
---|---|---|
GitHub CI/CD | ✅ | ⚠️ |
CLI | ✅ | ✅ |
Web Interface | ✅ | ❌ |
Package Publishing | ✅ | ✅ |
Product Documentation | ✅ | ✅ |
Server Stubs | ❌ | ✅ |
OpenAPI validation | ✅ | ❌ |
OpenAPI overlays | ✅ | ❌ |
AI-Powered spec edits | ✅ | ❌ |
⚠️ Fern claims CI/CD support for SDKs on their paid plan, but it is not in their documentation.
Enterprise Support
Speakeasy sets up tracking on all customer repositories and will proactively triage any issues that arise.
Feature | Speakeasy | Fern |
---|---|---|
Concierge onboarding | ✅ | ✅ |
Private slack channel | ✅ | ✅ |
Enterprise SLAs | ✅ | ✅ |
User issues triage | ✅ | ❌ |
Pricing
The biggest difference between the two pricing models is the starter plan. Fern offers the first SDK free but with a 20-endpoint cap, whereas Speakeasy's free tier is uncapped on the number of endpoints.
Plan | Speakeasy | Fern |
---|---|---|
Starter | 1 free Published SDK | 1 free local SDK; max 20 endpoints |
Scaleup | 1 free + $250/mo/SDK; max 200 endpoints | $250/mo/SDK; max 250 endpoints |
Enterprise | Custom | Custom |
Fern and Speakeasy Walkthrough
First, we'll show you the commands we used to create SDKs and documentation in Fern and Speakeasy. This is well explained in the documentation, so we'll keep it brief.
Both services support Linux, macOS, and Windows, and run in Docker.
We used the Speakeasy bar example for OpenAPI 3.1, available here (opens in a new tab)
Creating SDKs
Fern Quickstart
Follow the Fern quickstart here (opens in a new tab).
In a folder with the api.yaml
file for the schema, open a terminal and use Node.js with npm:
npm install -g fern-apifern init --openapi ./api.yaml;# will require github login in browserfern generate
init
creates afern
folder containing a copy of the OpenAPI schema and some configuration files.generate
creates SDKs in the folder../generated
. You can change the output folder by editinggenerators.yaml
. We used the following file to create all four languages:
default-group: localgroups:local: generators: - name: fernapi/fern-typescript-node-sdk version: 0.7.2 output: location: local-file-system path: ./generated/typescript config: outputSourceFiles: true # output .ts instead of .js with definitions files - name: fernapi/fern-python-sdk version: 0.7.2 output: location: local-file-system path: ./generated/python - name: fernapi/fern-java-sdk version: 0.5.15 output: location: local-file-system path: ../generated/java - name: fernapi/fern-go-sdk version: 0.9.2 output: location: local-file-system path: ../generated/go - name: fernapi/fern-postman version: 0.0.45 output: location: local-file-system path: ./generated/postman
init --docs
creates adocs.yml
configuration file.generate --docs;
creates documentation at the URL specified in the configuration file.
Speakeasy Quickstart
Follow the Speakeasy quickstart here (opens in a new tab).
The Speakeasy CLI is a single executable file built with Go (opens in a new tab).
brew install speakeasy-api/homebrew-tap/speakeasyspeakeasy quickstart
- Speakeasy handles authentication with a secret key in an environment variable, which you can get on the Speakeasy website.
- The Speakeasy quickstart will present an interactive mode that will walk you through creating an SDK.
Comparing Fern and Speakeasy's TypeScript Generation
Comparing the output of Fern and Speakeasy for all four SDK languages Fern supports would be too long for this article. We'll focus on TypeScript (JavaScript).
SDK Structure
Below is the Fern folder structure.
├── Client.d.ts├── Client.js├── api│ ├── errors│ │ ├── BadRequestError.d.ts│ │ ├── BadRequestError.js│ │ ├── UnauthorizedError.d.ts│ │ ├── UnauthorizedError.js│ │ ├── index.d.ts│ │ └── index.js│ ├── index.d.ts│ ├── index.js│ ├── resources│ │ ├── authentication│ │ │ ├── client│ │ │ │ ├── Client.d.ts│ │ │ │ ├── Client.js│ │ │ │ ├── index.d.ts│ │ │ │ ├── index.js│ │ │ │ └── requests│ │ │ │ ├── LoginRequest.d.ts│ │ │ │ ├── LoginRequest.js│ │ │ │ ├── index.d.ts│ │ │ │ └── index.js│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ └── types│ │ │ ├── LoginRequestType.d.ts│ │ │ ├── LoginRequestType.js│ │ │ ├── LoginResponse.d.ts│ │ │ ├── LoginResponse.js│ │ │ ├── index.d.ts│ │ │ └── index.js│ │ ├── config│ │ │ ├── client│ │ │ │ ├── Client.d.ts│ │ │ │ ├── Client.js│ │ │ │ ├── index.d.ts│ │ │ │ └── index.js│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ └── types│ │ │ ├── SubscribeToWebhooksRequestItem.d.ts│ │ │ ├── SubscribeToWebhooksRequestItem.js│ │ │ ├── index.d.ts│ │ │ └── index.js│ │ ├── drinks│ │ │ ├── client│ │ │ │ ├── Client.d.ts│ │ │ │ ├── Client.js│ │ │ │ ├── index.d.ts│ │ │ │ ├── index.js│ │ │ │ └── requests│ │ │ │ ├── ListDrinksRequest.d.ts│ │ │ │ ├── ListDrinksRequest.js│ │ │ │ ├── index.d.ts│ │ │ │ └── index.js│ │ │ ├── index.d.ts│ │ │ └── index.js│ │ ├── index.d.ts│ │ ├── index.js│ │ ├── ingredients│ │ │ ├── client│ │ │ │ ├── Client.d.ts│ │ │ │ ├── Client.js│ │ │ │ ├── index.d.ts│ │ │ │ ├── index.js│ │ │ │ └── requests│ │ │ │ ├── ListIngredientsRequest.d.ts│ │ │ │ ├── ListIngredientsRequest.js│ │ │ │ ├── index.d.ts│ │ │ │ └── index.js│ │ │ ├── index.d.ts│ │ │ └── index.js│ │ └── orders│ │ ├── client│ │ │ ├── Client.d.ts│ │ │ ├── Client.js│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ └── requests│ │ │ ├── CreateOrderRequest.d.ts│ │ │ ├── CreateOrderRequest.js│ │ │ ├── index.d.ts│ │ │ └── index.js│ │ ├── index.d.ts│ │ └── index.js│ └── types│ ├── ApiError.d.ts│ ├── ApiError.js│ ├── BadRequest.d.ts│ ├── BadRequest.js│ ├── Drink.d.ts│ ├── Drink.js│ ├── DrinkType.d.ts│ ├── DrinkType.js│ ├── Error_.d.ts│ ├── Error_.js│ ├── Ingredient.d.ts│ ├── Ingredient.js│ ├── IngredientType.d.ts│ ├── IngredientType.js│ ├── Order.d.ts│ ├── Order.js│ ├── OrderStatus.d.ts│ ├── OrderStatus.js│ ├── OrderType.d.ts│ ├── OrderType.js│ ├── index.d.ts│ └── index.js├── core│ ├── fetcher│ │ ├── APIResponse.d.ts│ │ ├── APIResponse.js│ │ ├── Fetcher.d.ts│ │ ├── Fetcher.js│ │ ├── Supplier.d.ts│ │ ├── Supplier.js│ │ ├── createRequestUrl.d.ts│ │ ├── createRequestUrl.js│ │ ├── getFetchFn.d.ts│ │ ├── getFetchFn.js│ │ ├── getHeader.d.ts│ │ ├── getHeader.js│ │ ├── getRequestBody.d.ts│ │ ├── getRequestBody.js│ │ ├── getResponseBody.d.ts│ │ ├── getResponseBody.js│ │ ├── index.d.ts│ │ ├── index.js│ │ ├── makeRequest.d.ts│ │ ├── makeRequest.js│ │ ├── requestWithRetries.d.ts│ │ ├── requestWithRetries.js│ │ ├── signals.d.ts│ │ ├── signals.js│ │ └── stream-wrappers│ │ ├── Node18UniversalStreamWrapper.d.ts│ │ ├── Node18UniversalStreamWrapper.js│ │ ├── NodePre18StreamWrapper.d.ts│ │ ├── NodePre18StreamWrapper.js│ │ ├── UndiciStreamWrapper.d.ts│ │ ├── UndiciStreamWrapper.js│ │ ├── chooseStreamWrapper.d.ts│ │ └── chooseStreamWrapper.js│ ├── index.d.ts│ ├── index.js│ ├── runtime│ │ ├── index.d.ts│ │ ├── index.js│ │ ├── runtime.d.ts│ │ └── runtime.js│ └── schemas│ ├── Schema.d.ts│ ├── Schema.js│ ├── builders│ │ ├── date│ │ │ ├── date.d.ts│ │ │ ├── date.js│ │ │ ├── index.d.ts│ │ │ └── index.js│ │ ├── enum│ │ │ ├── enum.d.ts│ │ │ ├── enum.js│ │ │ ├── index.d.ts│ │ │ └── index.js│ │ ├── index.d.ts│ │ ├── index.js│ │ ├── lazy│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── lazy.d.ts│ │ │ ├── lazy.js│ │ │ ├── lazyObject.d.ts│ │ │ └── lazyObject.js│ │ ├── list│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── list.d.ts│ │ │ └── list.js│ │ ├── literals│ │ │ ├── booleanLiteral.d.ts│ │ │ ├── booleanLiteral.js│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── stringLiteral.d.ts│ │ │ └── stringLiteral.js│ │ ├── object│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── object.d.ts│ │ │ ├── object.js│ │ │ ├── objectWithoutOptionalProperties.d.ts│ │ │ ├── objectWithoutOptionalProperties.js│ │ │ ├── property.d.ts│ │ │ ├── property.js│ │ │ ├── types.d.ts│ │ │ └── types.js│ │ ├── object-like│ │ │ ├── getObjectLikeUtils.d.ts│ │ │ ├── getObjectLikeUtils.js│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── types.d.ts│ │ │ └── types.js│ │ ├── primitives│ │ │ ├── any.d.ts│ │ │ ├── any.js│ │ │ ├── boolean.d.ts│ │ │ ├── boolean.js│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── number.d.ts│ │ │ ├── number.js│ │ │ ├── string.d.ts│ │ │ ├── string.js│ │ │ ├── unknown.d.ts│ │ │ └── unknown.js│ │ ├── record│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── record.d.ts│ │ │ ├── record.js│ │ │ ├── types.d.ts│ │ │ └── types.js│ │ ├── schema-utils│ │ │ ├── JsonError.d.ts│ │ │ ├── JsonError.js│ │ │ ├── ParseError.d.ts│ │ │ ├── ParseError.js│ │ │ ├── getSchemaUtils.d.ts│ │ │ ├── getSchemaUtils.js│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── stringifyValidationErrors.d.ts│ │ │ └── stringifyValidationErrors.js│ │ ├── set│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── set.d.ts│ │ │ └── set.js│ │ ├── undiscriminated-union│ │ │ ├── index.d.ts│ │ │ ├── index.js│ │ │ ├── types.d.ts│ │ │ ├── types.js│ │ │ ├── undiscriminatedUnion.d.ts│ │ │ └── undiscriminatedUnion.js│ │ └── union│ │ ├── discriminant.d.ts│ │ ├── discriminant.js│ │ ├── index.d.ts│ │ ├── index.js│ │ ├── types.d.ts│ │ ├── types.js│ │ ├── union.d.ts│ │ └── union.js│ ├── index.d.ts│ ├── index.js│ └── utils│ ├── MaybePromise.d.ts│ ├── MaybePromise.js│ ├── addQuestionMarksToNullableProperties.d.ts│ ├── addQuestionMarksToNullableProperties.js│ ├── createIdentitySchemaCreator.d.ts│ ├── createIdentitySchemaCreator.js│ ├── entries.d.ts│ ├── entries.js│ ├── filterObject.d.ts│ ├── filterObject.js│ ├── getErrorMessageForIncorrectType.d.ts│ ├── getErrorMessageForIncorrectType.js│ ├── isPlainObject.d.ts│ ├── isPlainObject.js│ ├── keys.d.ts│ ├── keys.js│ ├── maybeSkipValidation.d.ts│ ├── maybeSkipValidation.js│ ├── partition.d.ts│ └── partition.js├── environments.d.ts├── environments.js├── errors│ ├── NdimaresApiError.d.ts│ ├── NdimaresApiError.js│ ├── NdimaresApiTimeoutError.d.ts│ ├── NdimaresApiTimeoutError.js│ ├── index.d.ts│ └── index.js├── index.d.ts├── index.js└── serialization ├── index.d.ts ├── index.js ├── resources │ ├── authentication │ │ ├── client │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── requests │ │ │ ├── LoginRequest.d.ts │ │ │ ├── LoginRequest.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── index.d.ts │ │ ├── index.js │ │ └── types │ │ ├── LoginRequestType.d.ts │ │ ├── LoginRequestType.js │ │ ├── LoginResponse.d.ts │ │ ├── LoginResponse.js │ │ ├── index.d.ts │ │ └── index.js │ ├── config │ │ ├── client │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── subscribeToWebhooks.d.ts │ │ │ └── subscribeToWebhooks.js │ │ ├── index.d.ts │ │ ├── index.js │ │ └── types │ │ ├── SubscribeToWebhooksRequestItem.d.ts │ │ ├── SubscribeToWebhooksRequestItem.js │ │ ├── index.d.ts │ │ └── index.js │ ├── drinks │ │ ├── client │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── listDrinks.d.ts │ │ │ └── listDrinks.js │ │ ├── index.d.ts │ │ └── index.js │ ├── index.d.ts │ ├── index.js │ ├── ingredients │ │ ├── client │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── listIngredients.d.ts │ │ │ └── listIngredients.js │ │ ├── index.d.ts │ │ └── index.js │ └── orders │ ├── client │ │ ├── createOrder.d.ts │ │ ├── createOrder.js │ │ ├── index.d.ts │ │ └── index.js │ ├── index.d.ts │ └── index.js └── types ├── ApiError.d.ts ├── ApiError.js ├── BadRequest.d.ts ├── BadRequest.js ├── Drink.d.ts ├── Drink.js ├── DrinkType.d.ts ├── DrinkType.js ├── Error_.d.ts ├── Error_.js ├── Ingredient.d.ts ├── Ingredient.js ├── IngredientType.d.ts ├── IngredientType.js ├── Order.d.ts ├── Order.js ├── OrderStatus.d.ts ├── OrderStatus.js ├── OrderType.d.ts ├── OrderType.js ├── index.d.ts └── index.js
Below is the Speakeasy folder.
├── CONTRIBUTING.md├── FUNCTIONS.md├── README.md├── RUNTIMES.md├── USAGE.md├── docs│ ├── lib│ │ └── utils│ │ └── retryconfig.md│ ├── models│ │ ├── callbacks│ │ │ └── createorderorderupdaterequestbody.md│ │ ├── components│ │ │ ├── drink.md│ │ │ ├── drinktype.md│ │ │ ├── errort.md│ │ │ ├── ingredient.md│ │ │ ├── ingredienttype.md│ │ │ ├── order.md│ │ │ ├── ordertype.md│ │ │ ├── security.md│ │ │ └── status.md│ │ ├── errors│ │ │ ├── apierror.md│ │ │ └── badrequest.md│ │ ├── operations│ │ │ ├── createorderrequest.md│ │ │ ├── createorderresponse.md│ │ │ ├── getdrinkrequest.md│ │ │ ├── getdrinkresponse.md│ │ │ ├── listdrinksrequest.md│ │ │ ├── listdrinksresponse.md│ │ │ ├── listdrinkssecurity.md│ │ │ ├── listingredientsrequest.md│ │ │ ├── listingredientsresponse.md│ │ │ ├── loginrequestbody.md│ │ │ ├── loginresponse.md│ │ │ ├── loginresponsebody.md│ │ │ ├── loginsecurity.md│ │ │ ├── requestbody.md│ │ │ ├── type.md│ │ │ └── webhook.md│ │ └── webhooks│ │ └── stockupdaterequestbody.md│ └── sdks│ ├── authentication│ │ └── README.md│ ├── config│ │ └── README.md│ ├── drinks│ │ └── README.md│ ├── ingredients│ │ └── README.md│ ├── orders│ │ └── README.md│ └── sdk│ └── README.md├── jsr.json├── package.json├── src│ ├── core.ts│ ├── funcs│ │ ├── authenticationLogin.ts│ │ ├── configSubscribeToWebhooks.ts│ │ ├── drinksGetDrink.ts│ │ ├── drinksListDrinks.ts│ │ ├── ingredientsListIngredients.ts│ │ └── ordersCreateOrder.ts│ ├── hooks│ │ ├── hooks.ts│ │ ├── index.ts│ │ ├── registration.ts│ │ └── types.ts│ ├── index.ts│ ├── lib│ │ ├── base64.ts│ │ ├── config.ts│ │ ├── dlv.ts│ │ ├── encodings.ts│ │ ├── files.ts│ │ ├── http.ts│ │ ├── is-plain-object.ts│ │ ├── logger.ts│ │ ├── matchers.ts│ │ ├── primitives.ts│ │ ├── retries.ts│ │ ├── schemas.ts│ │ ├── sdks.ts│ │ ├── security.ts│ │ └── url.ts│ ├── models│ │ ├── callbacks│ │ │ ├── createorder.ts│ │ │ └── index.ts│ │ ├── components│ │ │ ├── drink.ts│ │ │ ├── drinkinput.ts│ │ │ ├── drinktype.ts│ │ │ ├── error.ts│ │ │ ├── index.ts│ │ │ ├── ingredient.ts│ │ │ ├── ingredientinput.ts│ │ │ ├── ingredienttype.ts│ │ │ ├── order.ts│ │ │ ├── orderinput.ts│ │ │ ├── ordertype.ts│ │ │ └── security.ts│ │ ├── errors│ │ │ ├── apierror.ts│ │ │ ├── badrequest.ts│ │ │ ├── httpclienterrors.ts│ │ │ ├── index.ts│ │ │ ├── sdkerror.ts│ │ │ └── sdkvalidationerror.ts│ │ ├── operations│ │ │ ├── createorder.ts│ │ │ ├── getdrink.ts│ │ │ ├── index.ts│ │ │ ├── listdrinks.ts│ │ │ ├── listingredients.ts│ │ │ ├── login.ts│ │ │ └── subscribetowebhooks.ts│ │ └── webhooks│ │ ├── index.ts│ │ └── stockupdate.ts│ ├── sdk│ │ ├── authentication.ts│ │ ├── config.ts│ │ ├── drinks.ts│ │ ├── index.ts│ │ ├── ingredients.ts│ │ ├── orders.ts│ │ └── sdk.ts│ └── types│ ├── blobs.ts│ ├── constdatetime.ts│ ├── enums.ts│ ├── fp.ts│ ├── index.ts│ ├── operations.ts│ ├── rfcdate.ts│ └── streams.ts└── tsconfig.json
Speakeasy includes a documentation folder next to the SDK folder.
Speakeasy creates a complete npm package, with a package.json
file, that is ready to be published to the npm registry. With Fern, you have to do extra work to prepare for publishing.
The structure of the SDK also has some bearing on the DevEx. To call order functions in the SDKs, you would use api/resources/orders/Client.js
in Fern and src/sdk/orders.ts
in Speakeasy.
Example SDK Method
Let's take a look at the code for a single call, createOrder
, in Fern and Speakeasy.
Here's Fern:
/** * Create an order for a drink. */createOrder(request, requestOptions) { var _a; return __awaiter(this, void 0, void 0, function* () { const { callbackUrl, body: _body } = request; const _queryParams = new url_search_params_1.default(); if (callbackUrl != null) { _queryParams.append("callback_url", callbackUrl); } const _response = yield core.fetcher({ url: (0, url_join_1.default)((_a = (yield core.Supplier.get(this._options.environment))) !== null && _a !== void 0 ? _a : environments.NdimaresApiEnvironment.Default, "order"), method: "POST", headers: { Authorization: yield this._getAuthorizationHeader(), "X-Fern-Language": "JavaScript", }, contentType: "application/json", queryParameters: _queryParams, body: yield serializers.orders.createOrder.Request.jsonOrThrow(_body, { unrecognizedObjectKeys: "strip" }), timeoutMs: (requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.timeoutInSeconds) != null ? requestOptions.timeoutInSeconds * 1000 : 60000, }); if (_response.ok) { return yield serializers.Order.parseOrThrow(_response.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, breadcrumbsPrefix: ["response"], }); } if (_response.error.reason === "status-code") { throw new errors.NdimaresApiError({ statusCode: _response.error.statusCode, body: _response.error.body, }); } switch (_response.error.reason) { case "non-json": throw new errors.NdimaresApiError({ statusCode: _response.error.statusCode, body: _response.error.rawBody, }); case "timeout": throw new errors.NdimaresApiTimeoutError(); case "unknown": throw new errors.NdimaresApiError({ message: _response.error.errorMessage, }); } });}
And here's Speakeasy:
/** * Create an order. * * @remarks * Create an order for a drink. */export async function ordersCreateOrder( client$: SDKCore, request: operations.CreateOrderRequest, options?: RequestOptions): Promise< Result< operations.CreateOrderResponse, | errors.APIError | SDKError | SDKValidationError | UnexpectedClientError | InvalidRequestError | RequestAbortedError | RequestTimeoutError | ConnectionError >> { const input$ = request; const parsed$ = schemas$.safeParse( input$, (value$) => operations.CreateOrderRequest$outboundSchema.parse(value$), "Input validation failed" ); if (!parsed$.ok) { return parsed$; } const payload$ = parsed$.value; const body$ = encodeJSON$("body", payload$.RequestBody, { explode: true }); const path$ = pathToFunc("/order")(); const query$ = encodeFormQuery$({ callback_url: payload$.callback_url, }); const headers$ = new Headers({ "Content-Type": "application/json", Accept: "application/json", }); const security$ = await extractSecurity(client$.options$.security); const context = { operationID: "createOrder", oAuth2Scopes: [], securitySource: client$.options$.security, }; const securitySettings$ = resolveGlobalSecurity(security$); const requestRes = client$.createRequest$( context, { security: securitySettings$, method: "POST", path: path$, headers: headers$, query: query$, body: body$, timeoutMs: options?.timeoutMs || client$.options$.timeoutMs || -1, }, options ); if (!requestRes.ok) { return requestRes; } const request$ = requestRes.value; const doResult = await client$.do$(request$, { context, errorCodes: ["4XX", "5XX"], retryConfig: options?.retries || client$.options$.retryConfig, retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], }); if (!doResult.ok) { return doResult; } const response = doResult.value; const responseFields$ = { HttpMeta: { Response: response, Request: request$ }, }; const [result$] = await m$.match< operations.CreateOrderResponse, | errors.APIError | SDKError | SDKValidationError | UnexpectedClientError | InvalidRequestError | RequestAbortedError | RequestTimeoutError | ConnectionError >( m$.json(200, operations.CreateOrderResponse$inboundSchema), m$.fail("4XX"), m$.jsonErr("5XX", errors.APIError$inboundSchema), m$.json("default", operations.CreateOrderResponse$inboundSchema) )(response, { extraFields: responseFields$ }); if (!result$.ok) { return result$; } return result$;}
Type Safety
Both Fern and Speakeasy ensure that if the input is incorrect, the SDK will throw an error instead of silently giving you incorrect data.
Fern uses a custom data serialization validator to validate every object received by your SDK from the server. See an example of this in api/resources/pet/client/Client.ts
, where the line return await serializers.Pet.parseOrThrow(_response.body, {
calls into the core/schemas/builders
code.
Speakeasy uses Zod (opens in a new tab), an open-source validator. The benefit is the elimination of the custom serialization code.
File Streaming
Streaming file transmission allows servers and clients to do gradual processing, which is useful for playing videos or transforming long text files.
Fern supports file streaming (opens in a new tab) but with the use of a proprietary endpoint extension, x-fern-streaming: true
.
Speakeasy supports the Streams API (opens in a new tab) web standard automatically. You can use code like the following to upload and download large files:
const fileHandle = await openAsBlob("./src/sample.txt");const result = await sdk.upload({ file: fileHandle });
Fern-Generated SDK Case Study: Cohere TypeScript
Let's take a closer look at a real-world SDK generated by Fern for a more complete view of Fern's SDK generation. We inspected the Cohere TypeScript SDK (opens in a new tab) and here's what we found.
SDK Structure
The Cohere SDK's repository is deeply nested, reminiscent of older Java codebases. This may reflect the generator's codebase, or it may be due to the generator's templates being designed by developers who aren't TypeScript specialists.
There is a separation between core SDK code and API-specific code such as models and request methods, but internal SDK tools that hide behind layers of abstraction are not marked clearly as internal. This can lead to breaking changes in users' applications in the future.
Speakeasy addresses these problems by clearly separating core internal code into separate files, or marking individual code blocks as clearly as possible for internal use. Repository structure and comments follow the best practices for each SDK's target platform, as designed by specialists in each platform.
Data Validation Libraries
Both Speakeasy and Fern generate SDKs that feature runtime data validation. We've observed that Speakeasy uses Zod, a popular and thoroughly tested data validation and schema declaration library.
The Cohere TypeScript SDK, on the other hand, uses a custom Zod-like type-checking library, which ships as part of the SDK. Using a hand-rolled type library is a questionable practice for various reasons.
Firstly, it ships type inference code as part of the SDK, which adds significant complexity.
Here's an example of date type inference (opens in a new tab) using complex regular expression copied from Stack Overflow.
While Zod uses a similar regex-based approach to dates under the hood, we know that Zod's types and methods are widely used, tested by thousands of brilliant teams each day, and are supported by stellar documentation (opens in a new tab).
Furthermore, using Zod in SDKs created by Speakeasy allows users to include Zod as an external library when bundling their applications. This is what Speakeasy encourages, by including Zod as a peer dependency to the SDK.
A hand-rolled type library will almost certainly lead to safety issues that are challenging to debug and impossible to find answers for from other developers, as there is no community support.
Documentation
Apart from a short README, the Cohere TypeScript SDK does not include any documentation. This is in stark contrast with SDKs created by Speakeasy that contain copy-paste usage examples for all methods, and documentation for each model. Speakeasy SDKs are also supported by Zod's detailed and clear documentation regarding types and validation.
Readability
SDK method bodies in the Cohere SDK are extremely long, unclear, and contain repeated verbose response matching code. As a result, methods are difficult to read and understand at a glance.
Response matching in SDK methods involves long switch statements that are repeated in each method. The snippet below from the Cohere SDK is repeated multiple times.
In contrast, Speakeasy creates SDKs with improved readability by breaking SDK functionality into smaller, more focused methods, without hiding important steps behind multiple layers of abstraction.
Open Enums
Both Speakeasy and Fern generate SDKs that allow users to pass unknown values in fields that are defined as enums if the SDK is configured to do so. This is useful to keep legacy SDKs working when an API changes.
However, where Speakeasy SDKs clearly mark unknown enum values by wrapping them in an Unrecognized
type, SDKs generated by Fern use a type assertion. By not marking unrecognized enum values as such, Fern undermines the type safety TypeScript users rely on.
Consider the following OpenAPI component:
Based on this definition, Speakeasy will allow users to set the value of the BackgroundColor
string to yellow
, but will mark it as unrecognized. Here's an example of what this looks like in TypeScript:
In the Cohere SDK generated by Fern, we found this enum:
When we looked at the definition of core.serialization.enum_
, we found that any string value can be passed as a status, and would be represented as type Status
.
SDK and Bundle Size
Both Speakeasy and Fern SDKs include runtime data validation, which can increase the bundle size. However, Speakeasy SDKs are designed to be tree-shakable, so you can remove any unused code from the SDK before bundling it.
Speakeasy also exposes a standalone function for each API call, which allows you to import only the functions you need, further reducing the bundle size.
Creating Bundles
Let's compare the bundle sizes of the SDKs generated by Speakeasy and Fern.
Start by adding a speakeasy.ts
file that imports the Speakeasy SDK:
Next, add a fern.ts
file that imports the Fern SDK:
We'll use esbuild
to bundle the SDKs. First, install esbuild
:
npm install esbuild
Next, add a build.js
script that uses esbuild
to bundle the SDKs:
Run the build.js
script:
node build.js
This generates two bundles, dist/speakeasy.js
and dist/fern.js
, along with their respective metafiles.
Bundle Size Comparison
Now that we have two bundles, let's compare their sizes.
First, let's look at the size of the dist/speakeasy.js
bundle:
du -sh dist/speakeasy.js# Output# 76K dist/speakeasy.js
Next, let's look at the size of the dist/fern.js
bundle:
du -sh dist/fern.js# Output# 232K dist/fern.js
The SDK generated by Fern is significantly larger than that built with the SDK generated by Speakeasy.
We can use the metafiles generated by esbuild
to analyze the bundle sizes in more detail.
Analyzing Bundle Sizes
The metafiles generated by esbuild
contain detailed information about which source files contribute to each bundle's size, presented as a tree structure.
We used esbuild's online bundle visualizer (opens in a new tab) to analyze the bundle sizes.
Here's a summary of the bundle sizes:
The dist/speakeasy.js
bundle's largest contributor, at 72.3%, is the Zod library used for runtime data validation. The Zod library's tree-shaking capabilities are a work in progress, and future versions of SDKs are expected to have smaller bundle sizes.
The dist/fern.js
bundle includes bundled versions of node-fetch, polyfills, and other dependencies, which contribute to the larger bundle size. Fern's SDKs also include custom serialization code, and a validation library, which can increase the bundle size.
Bundling for the Browser
Speakeasy SDKs are designed to work in a range of environments, including the browser. To bundle an SDK for the browser, you can use a tool like esbuild
or webpack
.
Here's an example of how to bundle the Speakeasy SDK for the browser using esbuild
:
npx esbuild src/speakeasy.ts --bundle --minify --target=es2020 --platform=browser --outfile=dist/speakeasy-web.js
Doing the same for the Fern SDK generates an error, as the SDK is not designed to work in the browser out of the box.
Summary
Speakeasy's additional language support and SDK documentation make it a better choice than Fern for most users.
If you are interested in seeing how Speakeasy stacks up against other SDK generation tools, check out our post (opens in a new tab).