Product Updates

Generating OpenAPI SDKs in TypeScript the Speakeasy way

Anuraag Nalluri

Anuraag Nalluri

June 18, 2023

Featured blog post image

Generating OpenAPI SDKs in TypeScript the Speakeasy way

As TypeScript developers, we love OpenAPI. We like to think of it as a contract between our UI code and the server: If everyone sticks to the contract, we can focus on creating delightful user experiences without spending our days navigating each new API's quirks.

However, manually consuming and building clients based on OpenAPI specifications is hard to love. We would need to read YAML specs, code up clients by hand, continuously keep up with spec changes, and document our clients for internal or external developers—we know this spells disaster. Luckily, there is a thriving ecosystem of OpenAPI client generators for most languages, TypeScript included.

When we set out to generate TypeScript SDKs using the available open-source generators, we found that the generated SDKs often lacked crucial features.

At Speakeasy, we generate idiomatic client SDKs (opens in a new tab) in a variety of languages. Our generators follow principles that ensure we generate SDKs that offer the best developer experience so that you can focus on building your API, and your developer-users can focus on delighting their users.

In this post, we'll compare a TypeScript SDK generated by Speakeasy to SDKs generated by open-source tools.

Here's what we found:

  • Precise documentation: Speakeasy is the only TypeScript SDK generator that creates documentation with working usage examples.
  • Strongly typed: SDKs generated by Speakeasy are fully typed. All models, fields, and parameters have types.
  • Concise, DRY code: Speakeasy generates metadata for all fields, allowing serializing and deserializing logic to access types at runtime without cluttering the SDK with duplicate code.
  • Retries and other utils included: Speakeasy is the only generator that adds utilities for retrying requests.

The TypeScript SDK generator landscape

We'll compare Speakeasy's SDK generator to three generators from the OpenAPI Generators project, and two additional open-source generators.

Our evaluation includes:

  1. The TypeScript Axios (opens in a new tab) generator from OpenAPI Generators.
  2. The TypeScript Fetch (opens in a new tab) generator from OpenAPI Generators.
  3. The TypeScript Node (opens in a new tab) generator from OpenAPI Generators.
  4. Oazapfts (opens in a new tab), an open-source generator with almost 400 stars on GitHub.
  5. OpenAPI TypeScript Codegen (opens in a new tab), a popular open-source generator with 1,700 stars on GitHub.
  6. The Speakeasy SDK generator.

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.

Let's install each generator and compare the process.

Installing the OpenAPI Generator CLI

OpenAPI Generator depends on Java, which we covered at length previously (opens in a new tab). We concluded that managing the OpenAPI Generator Java dependencies manually just wasn't worth the effort.

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

brew install openapi-generator

This adds the openapi-generator CLI to our path.

Installing other open-source SDK generators

The two independent generators in our comparison, oazapfts and OpenAPI TypeScript Codegen, both depend on the Node.js runtime for code generation and are best installed as part of a JavaScript project—with important differences between the two.

Oazapfts is installed as both a CLI and a runtime dependency. The SDK code generated by the oazapfts CLI excludes the HTTP client code and associated types but depends on oazapfts as a runtime library.

To install and save oazapfts, we'll run the following in the terminal:

# Create a new JavaScript project directory for petstore-sdk-oazapfts
mkdir petstore-sdk-oazapfts && cd petstore-sdk-oazapfts
 
# Create package.json
npm init -y
 
# Install oazapfts as a dependency
npm install oazapfts

This installs the oazapfts CLI and adds oazapfts as a dependency in your package.json.

To install OpenAPI TypeScript Codegen, we only need to install a CLI, but it is also installed via NPM, and the OpenAPI TypeScript Codegen documentation suggests saving openapi-typescript-codegen as a development dependency.

We'll install OpenAPI TypeScript Codegen by running the following in the terminal:

# Create a new JavaScript project directory for  petstore-sdk-otc
mkdir petstore-sdk-otc && cd petstore-sdk-otc
 
# Create package.json
npm init -y
 
# Install openapi-typescript-codegen and save it as a devDependency
npm install openapi-typescript-codegen --save-dev

Installing the Speakeasy CLI

The Speakeasy CLI is built with Go and installs as a single, dependency-free binary.

To install the Speakeasy CLI, we'll follow the steps in the Speakeasy Getting Started guide.

In the terminal, run:

brew install speakeasy-api/homebrew-tap/speakeasy

Next, authenticate with Speakeasy by running the following:

speakeasy auth login

Downloading the Swagger Petstore specification

Before we run our generators, we'll need an OpenAPI specification to generate a TypeScript SDK for. The standard specification for testing OpenAPI SDK generators and Swagger UI generators is the Swagger Petstore (opens in a new tab).

We'll download the YAML specification at https://petstore3.swagger.io/api/v3/openapi.yaml (opens in a new tab) to our working directory and name it petstore.yaml:

curl https://petstore3.swagger.io/api/v3/openapi.yaml --output petstore.yaml

Validating the spec

Both the OpenAPI Generator and Speakeasy CLI can validate an OpenAPI spec. Oazapfts and OpenAPI TypeScript Codegen don't offer validation, so if we were to use them at scale, a separate validation step would be required.

Validation using OpenAPI Generator

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

openapi-generator validate -i petstore.yaml

The OpenAPI Generator returns two warnings:

Warnings:
	- Unused model: Address
	- Unused model: Customer

[info] Spec has 2 recommendation(s).

Validation using Speakeasy

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

speakeasy validate openapi -s petstore.yaml

The Speakeasy validator returns ten warnings, seven hints that some methods don't specify any return values, and three unused components. Each warning includes a detailed JSON-formatted error with line numbers.

Since both validators validated the spec with only warnings, we can assume that all of our generators will generate SDKs without issues.

Comparing SDKs

Now that we know our OpenAPI spec is valid, we can start generating and comparing SDKs. First, we'll generate an SDK using Speakeasy and take a brief look at its structure. Then we'll generate SDKs using the open-source generators and compare the generated code to the Speakeasy SDK.

Generating an SDK using Speakeasy

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

# Generate Petstore SDK using Speakeasy TypeScript generator
speakeasy generate sdk \
    --schema petstore.yaml \
    --lang typescript \
    --out ./petstore-sdk-speakeasy/

The command above creates a new directory called petstore-sdk-speakeasy, with the following structure:

./
├── docs/
│   ├── pet/
│   │   └── README.md*
│   ├── sdk/
│   │   └── README.md*
│   ├── store/
│   │   └── README.md*
│   └── user/
│       └── README.md*
├── src/
│   ├── internal/
│   │   └── utils/
│   ├── sdk/
│   │   ├── models/
│   │   ├── types/
│   │   ├── index.ts*
│   │   ├── pet.ts*
│   │   ├── sdk.ts*
│   │   ├── store.ts*
│   │   └── user.ts*
│   └── index.ts*
├── README.md*
├── USAGE.md*
├── files.gen*
├── gen.yaml*
├── jest.config.js*
├── package-lock.json*
├── package.json*
└── tsconfig.json*

At a glance, we can see that Speakeasy generates documentation for each model in our schema and that it creates a full-featured NPM package. Code is split between internal tools and the SDK code.

We'll look at the generated code in more detail in our comparisons below, starting with OpenAPI Generator.

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 three TypeScript SDK generators from OpenAPI Generator we tried are typescript-axios (opens in a new tab), typescript-fetch (opens in a new tab), and typescript-node (opens in a new tab).

Usage is the same for all three generators, but we'll specify a unique output directory, generator name, and NPM project name for each.

We'll generate an SDK for each by running the following in the terminal:

# Generate Petstore SDK using typescript-axios generator
openapi-generator generate \
  --input-spec petstore.yaml \
  --generator-name typescript-axios \
  --output ./petstore-sdk-typescript-axios \
  --additional-properties=npmName=petstore-sdk-typescript-axios
 
# Generate Petstore SDK using typescript-fetch generator
openapi-generator generate \
  --input-spec petstore.yaml \
  --generator-name typescript-fetch \
  --output ./petstore-sdk-typescript-fetch \
  --additional-properties=npmName=petstore-sdk-typescript-fetch
 
# Generate Petstore SDK using typescript-node generator
openapi-generator generate \
  --input-spec petstore.yaml \
  --generator-name typescript-node \
  --output ./petstore-sdk-typescript-node \
  --additional-properties=npmName=petstore-sdk-typescript-node

Each command will output a list of files generated and create a unique directory. We specified an NPM package name as a configuration argument, npmName, for each generator. This argument is required for the generators to create full packages.

Speakeasy SDK compared to OpenAPI Generator SDKs

We inspected the generated SDKs and found that the Node and Fetch versions were the most complex in terms of number of files and lines of code. This seems counterintuitive because each of these two SDKs targets only one platform. Fetch is meant to be used in the browser, while the Node version is intended to be used with Node.js.

On closer inspection, the complexity of the Node and Fetch versions comes down to code repetition and verbosity. For example, this function from the Fetch SDK has the sole purpose of converting a Pet object to JSON:

// typescript-fetch JSON marshaling
export function PetToJSON(value?: Pet | null): any {
    if (value === undefined) {
        return undefined;
    }
    if (value === null) {
        return null;
    }
    return {
        
        'id': value.id,
        'name': value.name,
        'category': CategoryToJSON(value.category),
        'photoUrls': value.photoUrls,
        'tags': value.tags === undefined ? undefined : ((value.tags as Array<any>).map(TagToJSON)),
        'status': value.status,
    };
}

Speakeasy improves on this by adding metadata per field to create reusable marshaling logic for a variety of formats. This cuts down on repetitive boilerplate.

As an example, here's the Pet class generated by Speakeasy:

// Pet class generated by Speakeasy
export class Pet extends SpeakeasyBase {
    @SpeakeasyMetadata({ data: "form, name=category;json=true" })
    @Expose({ name: "category" })
    @Type(() => Category)
    category?: Category;
 
    @SpeakeasyMetadata({ data: "form, name=id" })
    @Expose({ name: "id" })
    id?: number;
 
    @SpeakeasyMetadata({ data: "form, name=name" })
    @Expose({ name: "name" })
    name: string;
 
    @SpeakeasyMetadata({ data: "form, name=photoUrls" })
    @Expose({ name: "photoUrls" })
    photoUrls: string[];
 
    /**
     * pet status in the store
     */
    @SpeakeasyMetadata({ data: "form, name=status" })
    @Expose({ name: "status" })
    status?: PetStatus;
 
    @SpeakeasyMetadata({ data: "form, name=tags;json=true", elemType: Tag })
    @Expose({ name: "tags" })
    @Type(() => Tag)
    tags?: Tag[];
}

Note the @SpeakeasyMetadata decorators that the Speakeasy SDK uses to serialize and deserialize data, depending on the use case. The example above includes instructions on how to serialize a Pet object and its related objects, Tag and Category. The @Expose and @Type decorators expose types at runtime.

This pattern is known as metadata reflection. The SpeakeasyMetadata function calls Reflect.defineMetadata from the reflect-metadata (opens in a new tab) package to handle reflection.

We found that the SDK generated by the Node generator contains dated patterns, such as creating string enums by casting string literals to <any>:

// Pet StatusEnum from typescript-node generator
export namespace Pet {
    export enum StatusEnum {
        Available = <any> 'available',
        Pending = <any> 'pending',
        Sold = <any> 'sold'
    }
}

Since TypeScript 2.4, string literals can be assigned to enum members to create string enums.

Generating an SDK with oazapfts

To run oazapfts, we'll either need to run it from the local JavaScript project's bin folder or install it globally. We opted to run it from the bin folder.

Navigate to the local JavaScript project we created for petstore-sdk-oazapfts, then run the following:

$(npm bin)/oazapfts ../petstore.yaml index.ts

Oazapfts runs without any output and generates a single TypeScript file, index.ts. Remember that we had to install oazapfts as a runtime dependency. Let's see what gets called from the dependency:

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. We can look at the runtime dependencies from Oazapfts itself, to get an idea of the dependency graph:

This is an excerpt from the oazapfts package.json file:

{
  "dependencies": {
    "@apidevtools/swagger-parser": "^10.1.0",
    "lodash": "^4.17.21",
    "minimist": "^1.2.8",
    "swagger2openapi": "^7.0.8",
    "typescript": "^5.0.4"
  }
}

Some of these dependencies clearly relate to the generator itself. For example, we can assume that no SDK client would need access to swagger-parser at runtime.

The Speakeasy-generated SDK packages contain specific dependencies, which we keep to a minimum. Most notably, SDKs generated by Speakeasy depend on Axios (opens in a new tab). Using Axios allows the Speakeasy SDK to be used in the browser and with Node.js, with minimal difference between these two environments.

Oazapfts generates simple types and uses unions instead of enum types to list valid strings. For example, here's the Pet type from the oazapfts SDK:

// Pet type generated by oazapfts
export type Pet = {
    id?: number;
    name: string;
    category?: Category;
    photoUrls: string[];
    tags?: Tag[];
    status?: "available" | "pending" | "sold";
};

Compare this to the equivalent enum generated by Speakeasy:

/**
 * pet status in the store
 */
export enum PetStatus {
    Available = "available",
    Pending = "pending",
    Sold = "sold",
}

Note how the PetStatus enum generated by Speakeasy includes a clear comment and provides a clean interface to enumerate statuses at runtime, and helpful intelligent code completion while coding.

Generating an SDK with OpenAPI TypeScript Codegen

As with oazapfts, we'll need to run the OpenAPI TypeScript Codegen CLI from our NPM binaries location, where it is aliased as openapi.

Navigate to the petstore-sdk-otc JavaScript project and run:

$(npm bin)/openapi \
  -i ../petstore.yaml
  -o src/

OpenAPI TypeScript Codegen uses the fetch API for requests by default, so it's aimed at SDKs used in the browser. However, it can be configured to use Axios. We tried using Axios and noted that OpenAPI TypeScript Codegen does not create an NPM package with dependencies, so we had to manually install a version of Axios.

In contrast, Speakeasy manages dependencies on behalf of the developer when generating an SDK, eliminating the need to guess which version of a dependency to install.

With OpenAPI TypeScript Codegen, we see unions used to represent enumerable strings again. Note how the status argument below is defined using a union of strings:

// findPetsByStatus generated by OpenAPI TypeScript Codegen
    /**
     * Finds Pets by status
     * Multiple status values can be provided with comma-separated strings
     * @param status Status values that need to be considered for filter
     * @returns Pet successful operation
     * @throws ApiError
     */
    public static findPetsByStatus(
        status: 'available' | 'pending' | 'sold' = 'available',
    ): CancelablePromise<Array<Pet>> {
        return __request(OpenAPI, {
            method: 'GET',
            url: '/pet/findByStatus',
            query: {
                'status': status,
            },
            errors: {
                400: `Invalid status value`,
            },
        });
    }

Compare this to how Speakeasy uses an enum and adds metadata, enabling runtime reflection and enumeration of the status strings:

/**
 * Status values that need to be considered for filter
 */
export enum FindPetsByStatusStatusEnum {
  Available = "available",
  Pending = "pending",
  Sold = "sold",
}
 
export class FindPetsByStatusRequest extends SpeakeasyBase {
  /**
   * Status values that need to be considered for filter
   */
  @SpeakeasyMetadata({
    data: "queryParam, style=form;explode=true;name=status",
  })
  status?: FindPetsByStatusStatusEnum;
}

Retries

The SDK generated by Speakeasy can automatically retry failed network requests or retry requests based on specific error responses.

This provides a straightforward developer experience for error handling.

To enable this feature, we use the Speakeasy x-speakeasy-retries extension to the OpenAPI spec. We'll update the OpenAPI spec to add retries to the addPet operation as a test.

Edit petstore.yaml and add the following to the addPet 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:
  /pet:
    # ...
    post:
      #...
      operationId: addPet
      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 for failed network requests when creating a new pet. It is also possible to enable retries for the SDK as a whole by adding global x-speakeasy-retries at the root of the OpenAPI spec.

Generated documentation

Of all the generators tested, Speakeasy was the only one to generate documentation and usage examples for its SDK. We see documentation generation as a crucial feature if you plan to publish your SDK to NPM for others to use.

Speakeasy generates a README.md at the root of the SDK, which we can customize (opens in a new tab) to add branding, support links, a code-of-conduct, and any other information our developer-users might find helpful.

The Speakeasy SDK also includes working usage examples for all operations, complete with imports and appropriately formatted string examples. For instance, if a type is formatted as email in our OpenAPI spec, Speakeasy generates usage examples with strings that look like email addresses. Types formatted as uri will generate examples that look like URLs. This makes example code clear and scannable.

Here's the usage example generated by Speakeasy after we update petstore.yaml to format the string items in photoUrls as uri:

import { SDK } from "openapi";
import { AddPetFormResponse } from "openapi/dist/sdk/models/operations";
import { PetStatusEnum } from "openapi/dist/sdk/models/shared";
 
const sdk = new SDK();
 
sdk.pet.addPetForm({
  category: {
    id: 1,
    name: "Dogs",
  },
  id: 10,
  name: "doggie",
  photoUrls: [
    "http://starchy-shorts.info",
    "https://soulful-poppy.com",
    "https://posh-muffin.com",
    "https://wasteful-route.name",
  ],
  status: PetStatusEnum.Available,
  tags: [
    {
      id: 473600,
      name: "Norma Ryan",
    },
    {
      id: 216550,
      name: "Brandon Auer",
    },
  ],
}, {
  petstoreAuth: "Bearer YOUR_ACCESS_TOKEN_HERE",
}).then((res: AddPetFormResponse) => {
  if (res.statusCode == 200) {
    // handle response
  }
});

Web interface and automation

This comparison focuses on the installation and usage of command line generators, but Speakeasy also offers features built around its SDK generator.

The Speakeasy web interface allows users to create TypeScript SDKs as complete packages that are checked into version control and automatically published to NPM.

The Speakeasy SDK generator user interface

The Speakeasy generator can also run as part of a CI workflow, for instance as a GitHub Action (opens in a new tab), to make sure your SDK is always up to date when your API spec changes.

A live example: Vessel API Node SDK

Vessel (opens in a new tab) trusts Speakeasy to generate and publish SDKs for its widely used APIs. We recently spoke to Zach Kirby about how Vessel uses Speakeasy (opens in a new tab). Zach shared that the Vessel Node SDK (opens in a new tab) 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're building an API that developers rely on and would like to publish full-featured client SDKs that follow best practices, we strongly recommend giving Speakeasy's SDK generator a try.

Join our Slack community (opens in a new tab) to let us know how we can improve our TypeScript SDK generator or to suggest features.