Speakeasy Logo
Skip to Content

Comparing OpenAPI TypeScript SDK Generators

At Speakeasy, idiomatic SDKs are created in a variety of languages, with generators that follow principles ensuring SDKs the best developer experience. The goal is to let developers focus on building great APIs and applications, without being distracted by hand-rolling custom SDKs just to get basic functionality.

In this post, we’ll compare TypeScript SDKs managed by Speakeasy to those generated by open-source generators.

The TypeScript SDK generator landscape

We’ll compare the Speakeasy SDK generator to some popular popular open-source generators.

Our evaluation includes:

  1. The TypeScript Fetch  generator from OpenAPI Generators.
  2. The TypeScript Node  generator from OpenAPI Generators.
  3. Oazapfts , an open-source generator with over 500 stars on GitHub.
  4. The Speakeasy SDK generator.

Here’s the summary of how the different generators compare:

Feature
Schema validation
Speakeasy
✅ Using Zod
TypeScript Fetch
✅ Basic
TypeScript Node
✅ Basic
Oazapfts
Documentation generation
Speakeasy
✅ Full docs and examples
TypeScript Fetch
TypeScript Node
Oazapfts
OpenAPI v3.1 support
Speakeasy
TypeScript Fetch
undefined
TypeScript Node
undefined
Oazapfts
Union types/polymorphism
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
✅ With discriminator
Browser support
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
Tree-shaking support
Speakeasy
TypeScript Fetch
⚠️ Limited
TypeScript Node
⚠️ Limited
Oazapfts
⚠️ Limited
OAuth 2.0
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
Retries
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
Pagination
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
React Hooks generation
Speakeasy
✅ With TanStack Query
TypeScript Fetch
TypeScript Node
Oazapfts
Data streaming
Speakeasy
✅ With runtime docs
TypeScript Fetch
TypeScript Node
Oazapfts
Node.js support
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
Deno support
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
Bun support
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
React Native support
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
Package publishing
Speakeasy
TypeScript Fetch
TypeScript Node
Oazapfts
CI/CD integration
Speakeasy
✅ GitHub Actions
TypeScript Fetch
TypeScript Node
Oazapfts

For a detailed comparison, read on.

Installing SDK generators

Although generator installation does not impact the resulting SDKs, your team will install the generator on each new development environment. We believe an emphasis on usability starts at home, and your internal tools should reflect this.

Install the Speakeasy CLI by running the Homebrew install command for macOS, or see the installation instructions for other platforms:

brew install speakeasy-api/tap/speakeasy

Installing openapi-generator using Homebrew installs openjdk@11 and its numerous dependencies:

brew install openapi-generator

Installing oazapfts is easiest done as an Node.js module with NPM or similar:

# Install oazapfts as a dependency npm install oazapfts --save

These generators will need an OpenAPI document to work with. A common OpenAPI document used for testing all sorts of OpenAPI tooling is the Train Travel API .

Start by downloading the YAML from https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/refs/heads/main/openapi.yaml  to the working directory.

wget https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/refs/heads/main/openapi.yaml

Document validation

Both the OpenAPI Generator and Speakeasy CLI can validate an OpenAPI document to make sure it’s valid and well-formed. Oazapfts doesn’t offer document validation, so a separate validation step is needed to use it at scale.

To validate openapi.yaml using OpenAPI Generator, run the following in the terminal:

openapi-generator validate -i openapi.yaml

The OpenAPI Generator validator returns the following output:

Validating spec (openapi.yaml) No validation issues detected.

Validation using Speakeasy

We’ll validate the spec with Speakeasy by running the following in the terminal:

speakeasy validate openapi -s openapi.yaml

The Speakeasy validator returns one warning, and some hints reminding the author to add examples. Each warning or hint includes a detailed, structured error with line numbers to help us fix anything that needs fixing.

Since the Speakeasy validator produced only a warning and hints, we can assume that all our generators will generate SDKs without issues.

Here’s how the generators’ validation features compare:

Validates schema
Speakeasy
OpenAPI Generator
Oazapfts
Shows line numbers
Speakeasy
OpenAPI Generator
Oazapfts
Helpful hints beyond validation
Speakeasy
OpenAPI Generator
Oazapfts

Generating SDKs

Now that the OpenAPI document has been confirmed valid, it’s time to start generating and comparing SDKs. First, create an SDK using Speakeasy, and take a brief look at its structure. Then generate SDKs using the other generators, and compare the generated code to the Speakeasy SDK.

Generating an SDK using Speakeasy

To create a TypeScript SDK using the Speakeasy CLI, run the following in the terminal:

speakeasy quickstart

It will ask a few questions about the SDK we want to create, including the OpenAPI document (openapi.yaml), the name of the SDK (TrainTravel), the language/framework which will be TypeScript, and a package name for publishing to NPM (train-travel-sdk). Then pick an output directory for the SDK, for example train-travel-sdk.

That’s it! The Speakeasy CLI generates the SDK, turns it into a Git repository if requested, and creates the following file structure:

├── CONTRIBUTING.md ├── FUNCTIONS.md ├── README.md ├── RUNTIMES.md ├── USAGE.md ├── dist │ ├── commonjs │ ├── esm │ └── node_modules ├── docs │ ├── lib │ ├── models │ └── sdks ├── eslint.config.mjs ├── examples │ ├── node_modules │ ├── package-lock.json │ ├── package.json │ ├── README.md │ └── stationsGetStations.example.ts ├── src │ ├── core.ts │ ├── funcs │ ├── hooks │ ├── index.ts │ ├── lib │ ├── models │ ├── sdk │ └── types └── tsconfig.json

At a glance, we can see that Speakeasy creates documentation for each model in the API description. It also creates a full-featured NPM package, with all the Markdown files you’d expect to see in any open-source project.

Code is split between internal tools and the SDK code, and comes packaged ready for distribution to NPM with support for CommonJS and ES Modules.

We’ll start poking around the code to get a feel for how it all works, but first, let’s generate SDKs using the other generators.

Generating SDKs using OpenAPI Generator

OpenAPI Generator is an open-source collection of community-maintained generators. It features generators for a wide variety of client languages, and for some languages, there are multiple generators. TypeScript tops this list of languages with multiple generators, with 11 options to choose from.

The two TypeScript SDK generators from OpenAPI Generator covered here are typescript-fetch  and typescript-node . Both generators are very similar, but the typescript-fetch generator creates SDKs that work in both browser and Node.js environments, while the typescript-node generator creates SDKs optimized for Node.js environments.

There is no interactive CLI for OpenAPI Generator, so there are no prompts to guide you on the way. Instead you’ll do the whole thing with command line arguments:

# Generate Train Travel SDK using typescript-fetch generator openapi-generator generate \ --input-spec openapi.yaml \ --generator-name typescript-fetch \ --output ./train-travel-sdk-typescript-fetch \ --additional-properties=npmName=train-travel-sdk-typescript-fetch # Generate Train Travel SDK using typescript-node generator openapi-generator generate \ --input-spec openapi.yaml \ --generator-name typescript-node \ --output ./train-travel-sdk-typescript-node \ --additional-properties=npmName=train-travel-sdk-typescript-node

Once run there will be lots of output as OpenAPI Generator churns through the document and generates the SDK, with warnings and output about unsafe access to caffeine… but that’s just Java being Java. Ignore all that and look for something like:

# Thanks for using OpenAPI Generator. # We appreciate your support!

If they both worked there will be a list of files generated in each output directory. Let’s take a look at the file structure of each generated SDK.

The typescript-fetch generator creates the following file structure. There is no documentation or examples included, nor contributing guides or other supporting internal Markdown files. Only a README and the code itself are included.

# train-travel-sdk-typescript-fetch ├── package.json ├── README.md ├── src │ ├── apis │ ├── index.ts │ ├── models │ └── runtime.ts └── tsconfig.json

The typescript-node generator a much flatter structure, with no src/ directory, just an api and model directory. Similar to the typescript-fetch generator, there is no documentation or examples of any sort, and not even a README.

# train-travel-sdk-typescript-node ├── api ├── api.ts ├── model ├── package.json └── tsconfig.json

The code structure is quite different between the two generators, but looking through that comes a little later. There’s one more generator to try out.

Generating an SDK with oazapfts

Oazapfts is essentially a thin wrapper around Fetch API  with TypeScript type definitions. The SDK is generated as a single file, with no documentation, no examples, no package structure at all, so it’s all super minimalistic.

When oazapfts has been added to a project with npm install, it can be called with npm exec. This will take the OpenAPI document as one argument and the output .ts file as a second argument.

npm exec oazapfts openapi.yaml index.ts

The output TypeScript file index.ts will rely on oazapfts as a runtime dependency, which provides the necessary functionality for the SDK.

import * as Oazapfts from "oazapfts/lib/runtime"; import * as QS from "oazapfts/lib/runtime/query";

Code generated by oazapfts excludes the HTTP client code, error handling, and serialization. This means that oazapfts relies on the runtime library to provide these features. This keeps the generated code small, but it also means that the SDK cannot be used without the runtime library and its dependencies.

Comparing generated code

Let’s take a look at how each of the SDK generators handles the same OpenAPI document, and seeing as this is TypeScript lets start with type definitions. To keep things interesting the example we’ll focus on is a polymorphic model. Polymorphism is about representing different types that share a common interface. In OpenAPI, polymorphic objects are represented using oneOf sub-schemas and sometimes the discriminator object.

Here’s a slightly trimmed down example from the Train Travel API:

# components.schemas. BookingPayment: type: object properties: amount: type: number description: Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. currency: $ref: '#/components/schemas/Currency' description: Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. source: oneOf: - title: Card type: object properties: object: type: string const: card name: type: string number: type: string cvc: type: string writeOnly: true exp_month: type: integer format: int64 exp_year: type: integer format: int64 address_post_code: type: string required: - name - number - cvc - exp_month - exp_year - title: Bank Account type: object properties: object: const: bank_account type: string name: type: string number: type: string sort_code: type: string bank_name: type: string required: - name - number - bank_name

How will each generator handle this polymorphic object? Let’s find out!

Speakeasy type definitions

Speakeasy will generate union types for polymorphic objects, and use the discriminator to add runtime type casting for input and output objects. The following code is a snippet of types generated for the BookingPayment schema:

/** * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ export const Currency = { Bam: "bam", Bgn: "bgn", Chf: "chf", Eur: "eur", Gbp: "gbp", Nok: "nok", Sek: "sek", Try: "try", } as const; /** * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ export type Currency = ClosedEnum<typeof Currency>; /** * A bank account to take payment from. Must be able to make payments in the currency specified in the payment. */ export type BankAccount = { object?: "bank_account" | undefined; name: string; /** * The account number for the bank account, in string form. Must be a current account. */ number: string; /** * The sort code for the bank account, in string form. Must be a six-digit number. */ sortCode?: string | undefined; /** * The name of the bank associated with the routing number. */ bankName: string; }; /** * A card (debit or credit) to take payment from. */ export type Card = { object?: "card" | undefined; /** * Cardholder's full name as it appears on the card. */ name: string; /** * The card number, as a string without any separators. On read all but the last four digits will be masked for security. */ number: string; /** * Card security code, 3 or 4 digits usually found on the back of the card. */ cvc: string; /** * Two-digit number representing the card's expiration month. */ expMonth: number; /** * Four-digit number representing the card's expiration year. */ expYear: number; /** * Postal code associated with the card's billing address. */ addressPostCode?: string | undefined; }; /** * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */ export type Source = Card | BankAccount; /** * A payment for a booking. */ export type BookingPayment = { /** * Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */ amount?: number | undefined; /** * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ currency?: Currency | undefined; /** * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */ source?: Card | BankAccount | undefined; };

Reusing the descriptions as comments means the code is nicely decorated for anyone who goes prodding around, and by using docblock syntax it will be read by JS/TS documentation generators too.

The types are also defined and exported so they can be used in runtime code easily, instead of defined inline as many of the other generators do. This helps reuse throughout the rest of the SDK for request/responses, and allow for the most complex of scenarios to be handles easily.

export type CreateBookingPaymentResponseBody$Outbound = { id?: string | undefined; amount?: number | undefined; currency?: string | undefined; source?: Card$Outbound | BankAccount$Outbound | undefined; status?: string | undefined; links?: models.LinksBooking$Outbound | undefined; };

Oazapfts type definition

Over to oazapfts, which sticks to its minimalist approach and generates one type for the request and one type for the response, with anything inside that being defined in line. If there are lots of shared parameters between requests and responses then these will be repeated, and that makes documentation and code suffer, but it keeps things simple. As for polymorphism, oazapfts handles union types with runtime type casting.

export type BookingPaymentRead = { /** Unique identifier for the payment. This will be a unique identifier for the payment, and is used to reference the payment in other objects. */ id?: string; /** Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */ amount?: number; /** Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try"; /** The status of the payment, one of `pending`, `succeeded`, or `failed`. */ status?: "pending" | "succeeded" | "failed"; }; export type BookingPaymentWrite = { /** Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */ amount?: number; /** Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try"; /** The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */ source?: { "object"?: "card"; /** Cardholder's full name as it appears on the card. */ name: string; /** The card number, as a string without any separators. On read all but the last four digits will be masked for security. */ "number": string; /** Card security code, 3 or 4 digits usually found on the back of the card. */ cvc: string; /** Two-digit number representing the card's expiration month. */ exp_month: number; /** Four-digit number representing the card's expiration year. */ exp_year: number; /** The postal code associated with the card's billing address. */ address_post_code?: string; } | { "object"?: "bank_account"; name: string; /** The account number for the bank account, in string form. Must be a current account. */ "number": string; /** The sort code for the bank account, in string form. Must be a six-digit number. */ sort_code?: string; /** The name of the bank associated with the routing number. */ bank_name: string; }; };

The verbosity of these types can be improved with the --mergeReadWriteOnly to combine the read and write models into one, but similar models with shared parameters will still be defining everything over again.

OpenAPI Generated typescript-fetch type definitions

OpenAPI Generated’s generated typescript-fetch SDK is much more verbose their either Speakeasy or Oazapfts. It does not seem too familiar with TypeScript and uses it rather loosely with a whole lot of if statements, and the bank account vs card payment logic really seems awkward.

/** * A card (debit or credit) to take payment from. * @export * @interface Card */ export interface Card { /** * * @type {string} * @memberof Card */ object?: CardObjectEnum; /** * Cardholder's full name as it appears on the card. * @type {string} * @memberof Card */ name: string; /** * The card number, as a string without any separators. On read all but the last four digits will be masked for security. * @type {string} * @memberof Card */ number: string; /** * Card security code, 3 or 4 digits usually found on the back of the card. * @type {string} * @memberof Card */ cvc: string; /** * Two-digit number representing the card's expiration month. * @type {number} * @memberof Card */ expMonth: number; /** * Four-digit number representing the card's expiration year. * @type {number} * @memberof Card */ expYear: number; /** * * @type {string} * @memberof Card */ addressPostCode?: string; } /** * @export */ export const CardObjectEnum = { Card: 'card' } as const; export type CardObjectEnum = typeof CardObjectEnum[keyof typeof CardObjectEnum]; /** * Check if a given object implements the Card interface. */ export function instanceOfCard(value: object): value is Card { if (!('name' in value) || value['name'] === undefined) return false; if (!('number' in value) || value['number'] === undefined) return false; if (!('cvc' in value) || value['cvc'] === undefined) return false; if (!('expMonth' in value) || value['expMonth'] === undefined) return false; if (!('expYear' in value) || value['expYear'] === undefined) return false; return true; } export function CardFromJSON(json: any): Card { return CardFromJSONTyped(json, false); } export function CardFromJSONTyped(json: any, ignoreDiscriminator: boolean): Card { if (json == null) { return json; } return { 'object': json['object'] == null ? undefined : json['object'], 'name': json['name'], 'number': json['number'], 'cvc': json['cvc'], 'expMonth': json['exp_month'], 'expYear': json['exp_year'], 'addressPostCode': json['address_post_code'] == null ? undefined : json['address_post_code'], }; } export function CardToJSON(json: any): Card { return CardToJSONTyped(json, false); } export function CardToJSONTyped(value?: Card | null, ignoreDiscriminator: boolean = false): any { if (value == null) { return value; } return { 'object': value['object'], 'name': value['name'], 'number': value['number'], 'cvc': value['cvc'], 'exp_month': value['expMonth'], 'exp_year': value['expYear'], 'address_post_code': value['addressPostCode'], }; }

It’s even managed to output some syntax errors and import some dependencies that were not used. Was it meant to use those imports somewhere, or is it bringing in unnecessary dependencies? Unclear.

OpenAPI Generator typescript-node type definitions

Finally, how about this OpenAPI Generator typescript-node template?

The typescript-node generator starts off looking simple enough, with a single type for any given payload:

/** * A payment for a booking. */ export class BookingPayment { /** * Unique identifier for the payment. This will be a unique identifier for the payment, and is used to reference the payment in other objects. */ 'id'?: string; /** * Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */ 'amount'?: number; /** * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ 'currency'?: BookingPayment.CurrencyEnum; 'source'?: BookingPaymentSource; /** * The status of the payment, one of `pending`, `succeeded`, or `failed`. */ 'status'?: BookingPayment.StatusEnum; static discriminator: string | undefined = undefined; static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ { "name": "id", "baseName": "id", "type": "string" }, { "name": "amount", "baseName": "amount", "type": "number" }, { "name": "currency", "baseName": "currency", "type": "BookingPayment.CurrencyEnum" }, { "name": "source", "baseName": "source", "type": "BookingPaymentSource" }, { "name": "status", "baseName": "status", "type": "BookingPayment.StatusEnum" } ]; static getAttributeTypeMap() { return BookingPayment.attributeTypeMap; } } export namespace BookingPayment { export enum CurrencyEnum { Bam = <any> 'bam', Bgn = <any> 'bgn', Chf = <any> 'chf', Eur = <any> 'eur', Gbp = <any> 'gbp', Nok = <any> 'nok', Sek = <any> 'sek', Try = <any> 'try' } export enum StatusEnum { Pending = <any> 'pending', Succeeded = <any> 'succeeded', Failed = <any> 'failed' } }

It’s hoisted some of the properties up into enums, and namespaced them which is nice. The polymorphic source property is defined as a separate type BookingPaymentSource in its own file, and here is how that looks:

import { RequestFile } from './models'; import { BankAccount } from './bankAccount'; import { Card } from './card'; /** * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */ export class BookingPaymentSource { 'object'?: BookingPaymentSource.ObjectEnum; 'name': string; /** * The account number for the bank account, in string form. Must be a current account. */ 'number': string; /** * Card security code, 3 or 4 digits usually found on the back of the card. */ 'cvc': string; /** * Two-digit number representing the card\'s expiration month. */ 'expMonth': number; /** * Four-digit number representing the card\'s expiration year. */ 'expYear': number; 'addressPostCode'?: string; /** * The sort code for the bank account, in string form. Must be a six-digit number. */ 'sortCode'?: string; /** * The name of the bank associated with the routing number. */ 'bankName': string; static discriminator: string | undefined = undefined; static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ { "name": "object", "baseName": "object", "type": "BookingPaymentSource.ObjectEnum" }, { "name": "name", "baseName": "name", "type": "string" }, { "name": "number", "baseName": "number", "type": "string" }, { "name": "cvc", "baseName": "cvc", "type": "string" }, { "name": "expMonth", "baseName": "exp_month", "type": "number" }, { "name": "expYear", "baseName": "exp_year", "type": "number" }, { "name": "addressPostCode", "baseName": "address_post_code", "type": "string" }, { "name": "sortCode", "baseName": "sort_code", "type": "string" }, { "name": "bankName", "baseName": "bank_name", "type": "string" } ]; static getAttributeTypeMap() { return BookingPaymentSource.attributeTypeMap; } } export namespace BookingPaymentSource { export enum ObjectEnum { BankAccount = <any> 'bank_account' } }

This is completely incorrect, as the oneOf for Card and BankAccount has been flattened into a single class with all the properties of both types. This means that when creating a BookingPaymentSource object, all properties from both Card and BankAccount are available, which is not the intended behavior at all.

Some older generators require the optional discriminator property in the OpenAPI document to handle scenarios that a oneOf should otherwise handle by itself, but even adding that doesn’t help here.

source: oneOf: - $ref: '#/components/schemas/Card' - $ref: '#/components/schemas/BankAccount' discriminator: propertyName: object

It still produces the exact same output.

Type generation summary

Here’s a summary of how each generator handles OpenAPI polymorphism:

Adds union types
Speakeasy
OG Fetch
OG Node
Oazapfts
Supports discriminator
Speakeasy
OG Fetch
OG Node
Oazapfts

Retries

The SDK managed by Speakeasy can automatically retry failed network requests or retry requests based on specific error responses, providing a straightforward developer experience for an otherwise complicated topic.

To enable this feature use the Speakeasy x-speakeasy-retries extension in the OpenAPI document. Here is an example updating openapi.yaml to add retries to the create-booking operation.

x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5

Add this snippet to the operation:

#... paths: /bookings: # ... post: #... operationId: create-booking x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5

Now we’ll rerun the Speakeasy generator to enable retries, and the SDK will automatically attempt to retry failed network requests when booking a trip.

It is also possible to enable retries for the SDK as a whole by adding a global x-speakeasy-retries at the root of the OpenAPI document instead of per operation.

React Hooks

React Hooks  are a popular way to manage state and side effects in React applications.

Speakeasy generates built-in React Hooks using TanStack Query . These hooks provide features like intelligent caching, type safety, pagination, and seamless integration with modern React patterns such as SSR and Suspense.

import { useQuery } from "@tanstack/react-query"; function BookShelf() { // loads books from an API const { data, status, error } = useQuery([ "books" // Cache key for the query ], async () => { const response = await fetch("https://api.example.com/books"); return response.json(); }); if (status === "loading") return <p>Loading books...</p>; if (status === "error") return <p>Error: {error?.message}</p>; return ( <ul> {data.map((book) => ( <li key={book.id}>{book.title}</li> ))} </ul> ); }

For example, in this basic implementation, the useQuery hook fetches data from an API endpoint. The cache key ensures unique identification of the query. The status variable provides the current state of the query: loading, error, or success. Depending on the query status, the component renders loading, error, or the fetched data as a list.

None of the other generators generate React Hooks for their SDKs.

React Hooks
Speakeasy
Node
Fetch
Oazapfts

For an in-depth look at how Speakeasy uses React Hooks, see our official release article .

Pagination

SDKs managed by Speakeasy include optional pagination for OpenAPI operations.

We’ll update our pet store schema to add an x-speakeasy-pagination extension and a page query parameter:

paths: /stations: get: x-speakeasy-pagination: type: offsetLimit inputs: - name: page in: parameters type: page outputs: results: $ parameters: - name: page in: query description: The offset to start from required: false schema: type: integer default: 0

After regenerating the SDK with Speakeasy, the get-stations operation is automatically paginated, and can be iterated through with async/await until the clients needs are met.

import { TrainTravel } from "train-travel-sdk"; const trainTravel = new TrainTravel({ oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "", }); async function run() { const result = await trainTravel.stations.get({ coordinates: "52.5200,13.4050", search: "Milano Centrale", country: "DE", }); for await (const page of result) { console.log(page); } } run();

None of the other generators include pagination as a feature, leaving it all to the API client developers to figure out.

Adds pagination
Speakeasy
OG Node
undefined
OG Fetch
undefined
Oazapfts

Streaming files & data

All the generators in our comparison generate SDKs that use the Fetch API , which enables streaming for large uploads or downloads. Speakeasy makes this clear by providing documentation showing how to use streaming for file uploads in the README. It’s important to show developer-users how to take advantage of this streaming, and helping them handle large file uploads in different runtimes will cut down on support interactions.

import { TrainTravel } from "train-travel-sdk"; const trainTravel = new TrainTravel({ oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "", }); async function run() { const result = await trainTravel.bookings.createRaw( bytesToStream( new TextEncoder().encode( "{\"trip_id\":\"4f4e4e1-c824-4d63-b37a-d8d698862f1d\",\"passenger_name\":\"John Doe\"}", ), ), ); console.log(result); } run();

Beyond simply uploading and download files with streaming, Speakeasy SDKs also support JSON streaming for large JSON payloads using standards and conventions like JSONL  or ND-JSON . This is particularly useful when dealing with large datasets that may not fit into memory all at once. Speakeasy provides built-in support for JSON streaming, allowing developers to process JSON data in chunks as it is received.

import { SDK } from '@speakeasy/sdk'; const sdk = new SDK(); async function streamLogs() { const result = await sdk.logs.fetch_logs(); for await (const event of result) { // Each event is a parsed JSON object from the stream console.log(`[${event.timestamp}] ${event.message}`); } } streamLogs().catch(error => { console.error('Error streaming logs:', error); });

OpenAPI Generator and Oazapfts do not support JSON streaming in their generated SDKs, which limits their ability to handle large JSON payloads efficiently.

There’s an extra issue with OpenAPI Generator’s typescript-node SDK, in that its content negotiation strategy (looking at Accept and Content-Type headers) is overly simplistic. It checks if the content type includes application/json with the line if (produces.indexOf('application/json') >= 0) { which is too broad, because application/jsonl will match that condition. If an API returns application/jsonl it will try to parse the response as a single JSON object instead of a stream of JSON objects, which will lead to runtime errors and frustrated developer-users.

Stream uploads
Speakeasy
OG Node
OG Fetch
Oazapfts
JSON streaming
Speakeasy
OG Node
OG Fetch
Oazapfts
Documentation for streaming
Speakeasy
OG Node
OG Fetch
Oazapfts

Generated documentation

Of all the generators tested, Speakeasy was the only one to generate documentation and usage examples for SDKs. Speakeasy considers documentation generation as a crucial feature to enable rapid adoption and ease of use when an SDK can be published to NPM, and not something that should be left to the API team to produce from scratch.

Adds documentation
Speakeasy
OG Node
OG Fetch
Oazapfts
Adds usage examples
Speakeasy
OG Node
OG Fetch
Oazapfts

Speakeasy generates a README.md generated at the root of the SDK, which you can customize to add branding, support links, a code of conduct, and any other information your developer-users might find helpful.

The Speakeasy SDK also includes working usage examples for all operations, complete with imports and appropriately formatted examples from the OpenAPI description. This is a huge help to developers getting started with the SDK, as they can copy and paste working code snippets directly into their applications. Here’s an example of an operation from the Train Travel SDK’s README.md:

import { TrainTravel } from "train-travel-sdk"; const trainTravel = new TrainTravel({ oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "", }); async function run() { const result = await trainTravel.stations.get({ coordinates: "52.5200,13.4050", search: "Milano Centrale", country: "DE", }); for await (const page of result) { console.log(page); } } run();

Bundling applications for the browser

Speakeasy creates SDKs that are tree-shakable  and can be bundled for the browser using tools like Webpack, Rollup, or esbuild.

Because Speakeasy supports a wider range of OpenAPI features, Speakeasy-created SDKs are likely to be slightly larger than those generated by other tools. Speakeasy also limits abstraction, which can lead to larger SDKs. This does not translate to a larger bundle size, as the SDK can be tree-shaken to remove unused code.

Any SDK that supports runtime type checking or validation will have a larger bundle size, but the benefits of type checking and validation far outweigh the cost of a slightly larger bundle. If you use the popular validation library Zod  in your application already, you can exclude it from the SDK bundle to reduce its size.

Here’s an example of how to exclude Zod from the SDK bundle:

npx esbuild src/speakeasy-app.ts \ --bundle \ --minify \ --target=es2020 \ --platform=browser \ --outfile=dist/speakeasy-app.js \ --external:zod

A live example: Vessel API Node SDK

Vessel  trusts Speakeasy to generate and publish SDKs for its widely used APIs. We recently spoke to Zach Kirby about how Vessel uses Speakeasy. Zach shared that the Vessel Node SDK  is downloaded from npm hundreds of times a week.

Summary

The open-source SDK generators we tested are all good and clearly took tremendous effort and community coordination to build and maintain. Different applications have widely differing needs, and smaller projects may not need all the features offered by Speakeasy.

If you are building an API that developers rely on and would like to publish full-featured SDKs that follow best practices, we strongly recommend giving the Speakeasy SDK generator a try.

Join our Slack community  to let us know how we can improve our TypeScript SDK generator or to suggest features.

Last updated on