Background image

In Depth: Speakeasy vs Fern

Nolan Sullivan

Nolan Sullivan

January 16, 2024

Featured blog post image

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:

  1. Fern is an SDK generation tool designed for the Fern domain-specific language (DSL). It creates SDKs in 4 languages and API reference documentation
  2. 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 just generate SDKs. You could use Fern for SDKs, Stoplight for documentation, Spectral for linting, and handroll your Terraform provider, or you could just 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.

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)!

LanguageSpeakeasyFern
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:

  1. 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.
  2. 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.
FeatureSpeakeasyFern
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:

  1. 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.
  2. Speakeasy offers a web interface for managing & monitoring the creation of your SDKs.
FeatureSpeakeasyFern
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.

FeatureSpeakeasyFern
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.

PlanSpeakeasyFern
Starter1 free Published SDK1 free local SDK; max 20 endpoints
Scaleup1 free + $250/mo/SDK; max 200 endpoints$250/mo/SDK; max 250 endpoints
EnterpriseCustomCustom

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-api
fern init --openapi ./api.yaml;
# will require github login in browser
fern generate

  • init creates a fern 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 editing generators.yaml. We used the following file to create all four languages:

default-group: local
groups:
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 a docs.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/speakeasy
speakeasy 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
│ │ ├── 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
│ │ │ │ ├── AuthenticateRequest.d.ts
│ │ │ │ ├── AuthenticateRequest.js
│ │ │ │ ├── index.d.ts
│ │ │ │ └── index.js
│ │ │ ├── index.d.ts
│ │ │ ├── index.js
│ │ │ └── types
│ │ │ ├── AuthenticateResponse.d.ts
│ │ │ ├── AuthenticateResponse.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
│ ├── 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
│ │ ├── index.d.ts
│ │ └── index.js
│ ├── index.d.ts
│ ├── index.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
│ │ │ ├── index.d.ts
│ │ │ ├── index.js
│ │ │ ├── stringLiteral.d.ts
│ │ │ └── stringLiteral.js
│ │ ├── object
│ │ │ ├── index.d.ts
│ │ │ ├── index.js
│ │ │ ├── object.d.ts
│ │ │ ├── object.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
│ │ │ ├── AuthenticateRequest.d.ts
│ │ │ ├── AuthenticateRequest.js
│ │ │ ├── index.d.ts
│ │ │ └── index.js
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ └── types
│ │ ├── AuthenticateResponse.d.ts
│ │ ├── AuthenticateResponse.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
├── 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. We omit the docs folder.


├── README.md
├── RUNTIMES.md
├── USAGE.md
├── docs
│ ├── # omitted
├── package-lock.json
├── package.json
├── src
│ ├── index.ts
│ ├── lib
│ │ ├── base64.ts
│ │ ├── config.ts
│ │ ├── encodings.ts
│ │ ├── event-streams.ts
│ │ ├── http.ts
│ │ ├── retries.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
│ │ │ ├── index.ts
│ │ │ └── sdkerror.ts
│ │ ├── operations
│ │ │ ├── authenticate.ts
│ │ │ ├── createorder.ts
│ │ │ ├── getdrink.ts
│ │ │ ├── index.ts
│ │ │ ├── listdrinks.ts
│ │ │ ├── listingredients.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
│ ├── decimal.ts
│ ├── index.ts
│ ├── operations.ts
│ └── rfcdate.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.

Notice the relative compactness of the Speakeasy SDK. Unnecessary code has been eliminated to keep bundle sizes small.

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.
*/
async createOrder(
requestBody: Array<components.OrderInput>,
callbackUrl?: string | undefined,
options?: RequestOptions
): Promise<operations.CreateOrderResponse> {
const input$: operations.CreateOrderRequest = {
requestBody: requestBody,
callbackUrl: callbackUrl,
};
const headers$ = new Headers();
headers$.set("user-agent", SDK_METADATA.userAgent);
headers$.set("Content-Type", "application/json");
headers$.set("Accept", "application/json");
const payload$ = operations.CreateOrderRequest$.outboundSchema.parse(input$);
const body$ = enc$.encodeJSON("body", payload$.RequestBody, { explode: true });
const path$ = this.templateURLComponent("/order")();
const query$ = [
enc$.encodeForm("callback_url", payload$.callback_url, {
explode: true,
charEncoding: "percent",
}),
]
.filter(Boolean)
.join("&");
let security$;
if (typeof this.options$.apiKey === "function") {
security$ = { apiKey: await this.options$.apiKey() };
} else if (this.options$.apiKey) {
security$ = { apiKey: this.options$.apiKey };
} else {
security$ = {};
}
const securitySettings$ = this.resolveGlobalSecurity(security$);
const response = await this.fetch$(
{
security: securitySettings$,
method: "POST",
path: path$,
headers: headers$,
query: query$,
body: body$,
},
options
);
const responseFields$ = {
ContentType: response.headers.get("content-type") ?? "application/octet-stream",
StatusCode: response.status,
RawResponse: response,
};
if (this.matchResponse(response, 200, "application/json")) {
const responseBody = await response.json();
const result = operations.CreateOrderResponse$.inboundSchema.parse({
...responseFields$,
Order: responseBody,
});
return result;
} else if (this.matchResponse(response, "5XX", "application/json")) {
const responseBody = await response.json();
const result = errors.APIError$.inboundSchema.parse({
...responseFields$,
...responseBody,
});
throw result;
} else if (this.matchResponse(response, "default", "application/json")) {
const responseBody = await response.json();
const result = operations.CreateOrderResponse$.inboundSchema.parse({
...responseFields$,
Error: responseBody,
});
return result;
} else {
const responseBody = await response.text();
throw new errors.SDKError("Unexpected API response", response, responseBody);
}
}

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 (opens in a new tab) 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 });

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).

CTA background illustrations

Speakeasy Changelog

Subscribe to stay up-to-date on Speakeasy news and feature releases.