Comparison guide: OpenAPI/Swagger TypeScript client generation
At Speakeasy, we create idiomatic SDKs in a variety of languages. Our generators follow principles that ensure we create 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 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 two generators from the OpenAPI Generators project and two additional popular open-source generators.
Our evaluation includes:
- The TypeScript Fetch (opens in a new tab) generator from OpenAPI Generators.
- The TypeScript Node (opens in a new tab) generator from OpenAPI Generators.
- Oazapfts (opens in a new tab), an open-source generator with almost 400 stars on GitHub.
- OpenAPI TypeScript Codegen (opens in a new tab), a popular open-source generator with 1,700 stars on GitHub.
- The Speakeasy SDK generator.
Here's the summary of how the different generators compare:
Feature | Speakeasy | TypeScript Fetch | TypeScript Node | Oazapfts | OpenAPI TypeScript Codegen |
---|---|---|---|---|---|
Schema Validation | ✅ using Zod | ✅ Basic | ✅ Basic | ❌ | ❌ |
Documentation Generation | ✅ Full docs & examples | ❌ | ❌ | ❌ | ❌ |
Union Types/Polymorphism | ✅ | ✅ | ❌ | ✅ With discriminator | ✅ Without discriminator |
Browser Support | ✅ | ✅ | ❌ | ✅ | ✅ |
Tree-shaking Support | ✅ | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited | ⚠️ Limited |
OAuth2.0 | ✅ | ❌ | ❌ | ❌ | ❌ |
Retries | ✅ | ❌ | ❌ | ❌ | ❌ |
Pagination | ✅ | ❌ | ❌ | ❌ | ❌ |
Data Streaming | ✅ With runtime docs | ✅ | ✅ | ✅ | ✅ |
Node.js Support | ✅ | ✅ | ✅ | ✅ | ✅ |
Deno Support | ✅ | ❌ | ❌ | ❌ | ❌ |
Bun Support | ✅ | ❌ | ❌ | ❌ | ❌ |
React Native Support | ✅ | ❌ | ❌ | ❌ | ❌ |
Package Publishing | ✅ | ❌ | ❌ | ❌ | ❌ |
CI/CD Integration | ✅ GitHub Actions | ❌ | ❌ | ❌ | ❌ |
For the 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 following in the terminal:
brew install speakeasy-api/homebrew-tap/speakeasy
Installing openapi-generator
using Homebrew installs openjdk@11
and its numerous dependencies:
brew install openapi-generator
To install oazapfts and openapi-typescript-codegen, add them to an npm package as dependencies:
# Install oazapfts as a dependencynpm install oazapfts --save# Install openapi-typescript-codegen and save it as a devDependencynpm install openapi-typescript-codegen --save-dev
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, we would need a separate validation step.
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 return values, and three unused components. Each warning includes a detailed, structured error with line numbers to help us fix validation errors.
Since both validators validated the spec with only warnings, we can assume that all our generators will generate SDKs without issues.
The Speakeasy validator includes an option to get hints on how to improve our schema. Use the --output-hints
argument to activate this feature:
speakeasy validate openapi --output-hints -s petstore.yaml
This provides a detailed list of hints, all of which would improve our eventual SDK users' experience.
Here's how the generators' validation features compare:
Speakeasy | openapi-gen | Oazapfts | Codegen | |
---|---|---|---|---|
Validates schema | ✅ | ✅ | ❌ | ❌ |
Shows line numbers | ✅ | ❌ | ❌ | ❌ |
Schema hints | ✅ | ❌ | ❌ | ❌ |
Generating SDKs
Now that we know our OpenAPI spec is valid, we can start generating and comparing SDKs. First, we'll create 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 create a TypeScript SDK using the Speakeasy CLI, run the following in the terminal:
# Create Petstore SDK using Speakeasy TypeScript generatorspeakeasy 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:
./├── README.md├── USAGE.md├── docs/│ ├── models/│ └── sdks/├── files.gen├── gen.yaml*├── package-lock.json├── package.json├── src/│ ├── index.ts│ ├── lib/│ ├── models/│ ├── sdk/│ └── types/└── tsconfig.json
At a glance, we can see that Speakeasy creates 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 two TypeScript SDK generators from OpenAPI Generator we tried are typescript-fetch (opens in a new tab) and typescript-node (opens in a new tab).
Usage is the same for both 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-fetch generatoropenapi-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 generatoropenapi-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.
Generating an SDK With Oazapfts
To run oazapfts, we'll either need to run it from the local JavaScript project 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
and 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.2.2" }}
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.
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.
By 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.
Polymorphism
The Petstore schema does not include examples of polymorphism in OpenAPI, so we'll add two new schemas for Dog
and Cat
, and use them as input and output in the updatePet
operation.
Add the following to the component schemas in petstore.yaml
:
components: schemas: Dog: allOf: - $ref: '#/components/schemas/Pet' - type: object properties: petType: type: string example: Dog bark: type: string xml: name: dog Cat: allOf: - $ref: '#/components/schemas/Pet' - type: object properties: petType: type: string example: Cat hunts: type: boolean age: type: integer format: int32 xml: name: cat
Then add discriminated unions (opens in a new tab) to the updatePet
operation:
paths: /pet: put: requestBody: content: application/json: schema: discriminator: propertyName: petType mapping: dog: '#/components/schemas/Dog' cat: '#/components/schemas/Cat' oneOf: - $ref: '#/components/schemas/Cat' - $ref: '#/components/schemas/Dog' responses: "200": application/json: schema: discriminator: propertyName: petType mapping: dog: '#/components/schemas/Dog' cat: '#/components/schemas/Cat' oneOf: - $ref: '#/components/schemas/Cat' - $ref: '#/components/schemas/Dog'
After regenerating the SDKs, let's inspect each SDK's updatePet
method.
We see that oazapfts generates a method that type casts the input and output objects based on the discriminating field petType
:
export function updatePet(body: ({ petType: "dog";} & Dog) | ({ petType: "cat";} & Cat), opts?: Oazapfts.RequestOpts) { return oazapfts.fetchJson<{ status: 200; data: ({ petType: "dog"; } & Dog) | ({ petType: "cat"; } & Cat); } | { status: 400; } | { status: 404; } | { status: 405; }>("/pet", oazapfts.json({ ...opts, method: "PUT", body }));}
The SDK generated by typescript-fetch is slightly more verbose, and uses switch statements to derive the types for input and output objects:
export type UpdatePetRequest = { petType: 'cat' } & Cat | { petType: 'dog' } & Dog;export function UpdatePetRequestFromJSON(json: any): UpdatePetRequest { return UpdatePetRequestFromJSONTyped(json, false);}export function UpdatePetRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): UpdatePetRequest { if ((json === undefined) || (json === null)) { return json; } switch (json['petType']) { case 'cat': return {...CatFromJSONTyped(json, true), petType: 'cat'}; case 'dog': return {...DogFromJSONTyped(json, true), petType: 'dog'}; default: throw new Error(`No variant of UpdatePetRequest exists with 'petType=${json['petType']}'`); }}export function UpdatePetRequestToJSON(value?: UpdatePetRequest | null): any { if (value === undefined) { return undefined; } if (value === null) { return null; } switch (value['petType']) { case 'cat': return CatToJSON(value); case 'dog': return DogToJSON(value); default: throw new Error(`No variant of UpdatePetRequest exists with 'petType=${value['petType']}'`); }}
OpenAPI Codegen adds a union for Cat
and Dog
on both input and output, but does not use the discriminator to add runtime type casting:
export class PetService { public static updatePet( requestBody: (Cat | Dog), ): CancelablePromise<(Cat | Dog)> { return __request(OpenAPI, { method: 'PUT', url: '/pet', body: requestBody, mediaType: 'application/json', errors: { 400: `Invalid ID supplied`, 404: `Pet not found`, 405: `Validation exception`, }, }); }}
The typescript-node generator does not make use of types for unions in OpenAPI:
export class UpdatePetRequest { 'id'?: number; 'name': string; 'category'?: Category; 'photoUrls': Array<string>; 'tags'?: Array<Tag>; /** * pet status in the store */ 'status'?: UpdatePetRequest.StatusEnum; 'petType'?: string; 'hunts'?: boolean; 'age'?: number; 'bark'?: string; // ...}
Here's a summary of how each generator handles OpenAPI polymorphism:
Speakeasy | Node | Fetch | Oazapfts | Codegen | |
---|---|---|---|---|---|
Adds union types | ✅ | ❌ | ✅ | ✅ | ✅ |
Uses discriminator | ✅ | ❌ | ✅ | ✅ | ❌ |
Retries
The SDK managed 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 in 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 a global x-speakeasy-retries
at the root of the OpenAPI spec.
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 an offset
query parameter:
paths: /store/inventory: get: x-speakeasy-pagination: type: offsetLimit inputs: - name: offset # This offset refers to the value called `offset` in: parameters # In this case, offset is an operation parameter (header, query, or path) type: offset # The offset parameter will be used as the offset, which will be incremented by the length of the `output.results` array outputs: results: $.data.resultArray # The length of data.resultsArray value of the response will be added to the `offset` value to determine the new offset parameters: - name: offset in: query description: The offset to start from required: false schema: type: integer default: 0
After regenerating the SDK with Speakeasy, the getInventory
operation includes pagination:
import { SDK } from "openapi";import { GetInventorySecurity } from "openapi/models/operations";async function run() { const sdk = new SDK(); const offset = 20; const operationSecurity: GetInventorySecurity = "<YOUR_API_KEY_HERE>"; const res = await sdk.store.getInventory(operationSecurity, offset); if (res?.statusCode !== 200) { throw new Error("Unexpected status code: " + res?.statusCode || "-"); } let items: typeof res | null = res; while (items != null) { // handle items items = await items.next(); }}run();
None of the other generators include pagination as a feature.
Speakeasy | Node | Fetch | Oazapfts | Codegen | |
---|---|---|---|---|---|
Adds pagination | ✅ | ❌ | ❌ | ❌ | ❌ |
Data Streaming
All the generators in our comparison generate SDKs that use the Fetch
API, which enables streaming for large uploads or downloads.
Speakeasy | Node | Fetch | Oazapfts | Codegen | |
---|---|---|---|---|---|
Stream uploads | ✅ | ✅ | ✅ | ✅ | ✅ |
Speakeasy creates detailed documentation as part of the SDK, detailing how to open large files on different runtimes to help your developer-users take advantage of streaming.
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.
Here's how the generators add documentation:
Speakeasy | Node | Fetch | Oazapfts | Codegen | |
---|---|---|---|---|---|
Adds documentation | ✅ | ❌ | ❌ | ❌ | ❌ |
Adds usage examples | ✅ | ❌ | ❌ | ❌ | ❌ |
Speakeasy generates a README.md
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 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 managed by Speakeasy after we update petstore.yaml
to format the string items in photoUrls
as uri
:
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 Zod elsewhere in your application, 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:
Automation
This comparison focuses on the installation and usage of command line generators, but 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. 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 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 (opens in a new tab) to let us know how we can improve our TypeScript SDK generator or to suggest features.