
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 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)!
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│ │ ├── 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).