In Depth: Speakeasy vs APIMatic
Nolan Sullivan
September 30, 2024
At Speakeasy, we create idiomatic SDKs in the most popular 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 created by Speakeasy to those generated by APIMatic.
TL;DR? Here’s what we found:
- Ease of installation: The Speakeasy CLI has a straightforward installation process, especially for macOS users, with a single dependency-free binary. APIMatic, while also straightforward, requires Node.js and is installed using npm, which might add complexity for some users.
- Code generation: In our test, the SDK generated by APIMatic via CLI had issues with the
src/clientInterface.ts
file. Although the APIMatic support team responded, the problem persisted in our Docker environment test. By contrast, Speakeasy generated a complete and error-free SDK on our first attempt. - Documentation and code structure: The Speakeasy-created SDK includes comprehensive documentation, and relies on Zod for runtime data parsing, making it easier for developers to understand and use. The structure and organization of the SDK were clear and intuitive.
- Developer experience and flexibility: Speakeasy seems to place a significant emphasis on the developer experience, offering more customization options and a focus on the API-Developer Experience (DevEx). This could be particularly beneficial for teams looking for greater control over their SDKs.
- Response to issues: While the APIMatic support team was responsive, the resolution provided did not address the issue effectively in our case. Since we encountered no issues creating an SDK with Speakeasy, there was no need to similarly test the Speakeasy support team’s responsiveness.
- Bundle sizes and browser compatibility: Even when including Zod for runtime type checking, bundles using Speakeasy’s SDKs are smaller than those created with APIMatic SDKs. This could be beneficial for developers who need to keep their bundle sizes small. Speakeasy’s SDKs are also compatible with modern browsers, which is essential for web developers.
NOTE
Speakeasy and APIMatic each have their strengths, but Speakeasy’s ease of installation, reliable code generation, and developer-focused features give it the edge.
Of course, individual experiences may vary based on specific needs and use cases, so you might want to follow our process below to test-drive both tools.
Comparing Speakeasy and APIMatic
Before we get into the technical walkthrough, let’s see whether each platform targets the languages your users require, offers features you want, and provides the support you can rely on.
SDK Generation Targets
At Speakeasy, we believe it is crucial to meet your users where they are by supporting SDKs in languages your users depend on. Anyone who has had to maintain custom SDK code because a vendor doesn’t support their tech stack knows how frustrating this can be.
This table shows the current, as of September 2024, languages and platforms targeted by Speakeasy and APIMatic. These lists will change over time, so check the official documentation for the latest language support.
Language | Speakeasy | APIMatic |
---|---|---|
Python | ✅ | ✅ |
TypeScript | ✅ | ✅ |
Go | ✅ | ✅ (Alpha) |
C# (.NET) | ✅ | ✅ |
PHP | ✅ | ✅ |
Ruby | ✅ | ✅ |
Java | ✅ | ✅ |
Kotlin | ⚠ Java is Kotlin-compatible | ❌ |
Terraform provider | ✅ | ❌ |
Swift | ✅ | ❌ |
Unity | ✅ | ❌ |
Postman Collection | ✅ | ❌ |
We’re always open to expanding our language support, but would only ever do this if we have the in-house experience to create idiomatic, best-in-class SDKs for a given language. Let us know if you would like to suggest a language or platform to support.
SDK Features
The table below compares the current SDK features offered by Speakeasy and APIMatic as of September 2024. Both Speakeasy and APIMatic are under active development, so these features may change over time.
Feature | Speakeasy | APIMatic |
---|---|---|
Union types | ✅ | ✅ |
Discriminated union types | ✅ | ⚠ non-OpenAPI standard |
Server-sent events | ✅ | ❌ |
Retries | ✅ | ✅ |
Pagination | ✅ | ❌ |
Async support | ✅ | ✅ |
Streaming uploads | ✅ | ❌ |
OAuth 2.0 | ✅ | ✅ |
Custom SDK naming | ✅ | ✅ |
Customize SDK structure | ✅ | ❌ |
Custom dependency injection | ✅ | ❌ |
APIMatic lacks advanced SDK customization features, and we couldn’t find any code or documentation related to pagination. These are features Speakeasy’s users rely on.
Platform Features
Speakeasy’s primary interface is an open-source, full-featured, and portable CLI. Developers use our CLI to experiment and iterate locally and to customize their CI/CD workflows.
APIMatic’s CLI depends on Node.js and several packages. This makes it much less portable. In testing, we also found that it does not generate SDKs as reliably as the APIMatic web interface.
Feature | Speakeasy | APIMatic |
---|---|---|
GitHub CI/CD | ✅ | ✅ |
CLI | ✅ | ⚠ |
Web interface | ✅ | ✅ |
Package publishing | ✅ | ✅ |
OpenAPI linting | ✅ | ✅ |
Documentation generation | ✅ | ✅ |
Test generation | ✅ | ✅ |
OpenAPI overlays | ✅ | ❌ |
Change detection | ✅ | ❌ |
Developer portal | ❌ | ✅ |
Enterprise Support
Both Speakeasy and APIMatic offer support for Enterprise customers. This includes features like concierge onboarding, private Slack channels, and enterprise SLAs.
Feature | Speakeasy | APIMatic |
---|---|---|
Concierge onboarding | ✅ | ✅ |
Private Slack channel | ✅ | ✅ |
Enterprise SLAs | ✅ | ✅ |
User issues triage | ✅ | ✅ |
Pricing
Speakeasy offers a free plan, while APIMatic offers a limited free trial.
Plan | Speakeasy | APIMatic |
---|---|---|
Free | 1 free Published SDK, 50 endpoints | Trial only |
Startup | 1 free + $250/mo/SDK, 50 endpoints each | N/A |
Lite: Starter | N/A | $15/mo. 1 API, 10 endpoints, no team members. |
Lite: Basic | N/A | Custom. 1 API, 20 endpoints, 2 team members. |
Business | N/A | Custom. Up to 50 APIs, 100 endpoints each, 15 team members. |
Enterprise | Custom | Custom |
Speakeasy’s free plan is more generous than both Lite plans offered by APIMatic.
Speakeasy vs APIMatic Technical Walkthrough
Let’s create SDKs with Speakeasy and APIMatic from a single API specification, to compare the output and customization features.
We’ve created an OpenAPI document that describes a fictional bookstore API. You can find the complete OpenAPI document in the example repository (opens in a new tab), but let’s take a look at what’s included.
Our bookstore OpenAPI document is compliant with OpenAPI 3.1, which is supported by both Speakeasy and APIMatic. We define a basic info section and add a single development server.
Here we define two tags to organize our operations with: Books
and Orders
.
We define one global authentication method, apiKey
.
Let’s examine the operations we’ll need an SDK for, starting with getAllBooks
.
This operation takes no input.
What makes this operation interesting is that it returns an array of objects of three types: ProgrammingBook
, FantasyBook
, and SciFiBook
. Each object’s type is determined by the book’s category.
This example allows us to test how our SDK generators handle discriminated unions in OpenAPI.
Next up, we have an operation that adds a book to the database, called addBook
.
This operation takes one object of type ProgrammingBook
, FantasyBook
, or SciFiBook
as input.
Our next book-related operation, updateBookCoverById
, takes a book ID as a path variable, and an image as a binary payload.
We include this operation to test how our SDK generators handle binary payloads.
Our final book-related operation, getBookById
, takes a book ID as a path variable, and returns one of our book objects.
Next up, we have an operation that returns a list of all orders in the database, called getAllOrders
.
This operation returns an array of Order
objects, so that we can test an array of nested objects.
Our next order-related operation, createOrder
, takes an object of type NewOrder
as input, and returns an object of type Order
.
We include this one to test how our SDK generators help users avoid common mistakes, like passing the wrong type to an operation.
Finally, we have an operation that returns a stream of order events, called getOrderStream
.
We include this operation to test how our SDK generators handle server-sent events.
The remainder of the OpenAPI document defines the components used in the operations above.
openapi: 3.1.0info:title: Bookstore APIdescription: API for a bookstore with categories Programming, Fantasy, and Sci-fiversion: 1.0.0contact:name: John Doeemail: john@example.comurl: https://example.comlicense:name: MITurl: https://opensource.org/licenses/MITservers:- url: http://127.0.0.1:4010description: Local Prism servertags:- name: Booksdescription: Operations related to books- name: Ordersdescription: Operations related to orderssecurity:- apiKey: []paths:/books:get:summary: Get all booksoperationId: getAllBooksdescription: Returns a list of bookssecurity:- clientCredentials:- books.readtags:- Booksresponses:"200":description: A list of bookscontent:application/json:schema:type: arrayitems:oneOf:- $ref: "#/components/schemas/ProgrammingBook"- $ref: "#/components/schemas/FantasyBook"- $ref: "#/components/schemas/SciFiBook"discriminator:propertyName: categorymapping:Programming: "#/components/schemas/ProgrammingBook"Fantasy: "#/components/schemas/FantasyBook"Sci-fi: "#/components/schemas/SciFiBook"examples:example1:summary: Programming book examplevalue:- id: 1title: Clean Codedescription: A Handbook of Agile Software Craftsmanshipprice: 2999category: Programmingauthor:id: 1name: Robert C. Martinphoto: https://example.com/photos/robert.jpgbiography: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...cover_image: https://example.com/covers/cleancode.jpg- id: 2title: The Hobbitdescription: A fantasy novel by J.R.R. Tolkienprice: 1599category: Fantasyauthor:id: 2name: J.R.R. Tolkienphoto: https://example.com/photos/tolkien.jpgbiography: John Ronald Reuel Tolkien was an English writer, poet, philologist, and academic...cover_image: https://example.com/covers/thehobbit.jpgpost:summary: Add a new bookoperationId: addBookdescription: Adds a new book to the bookstoresecurity:- apiKey: []tags:- BooksrequestBody:description: Book object to be addedrequired: truecontent:application/json:schema:oneOf:- $ref: "#/components/schemas/ProgrammingBook"- $ref: "#/components/schemas/FantasyBook"- $ref: "#/components/schemas/SciFiBook"discriminator:propertyName: categorymapping:Programming: "#/components/schemas/ProgrammingBook"Fantasy: "#/components/schemas/FantasyBook"Sci-fi: "#/components/schemas/SciFiBook"examples:example:summary: Example bookvalue:title: New Sci-Fi Bookdescription: A new Sci-Fi book descriptionprice: 1999category: Sci-fiauthor:name: New Authorphoto: https://example.com/photos/newauthor.jpgbiography: New Author is an upcoming writer in the Sci-Fi genre...cover_image: https://example.com/covers/newbook.jpgresponses:"201":description: Book created successfullycontent:application/json:schema:oneOf:- $ref: "#/components/schemas/ProgrammingBook"- $ref: "#/components/schemas/FantasyBook"- $ref: "#/components/schemas/SciFiBook"discriminator:propertyName: categorymapping:Programming: "#/components/schemas/ProgrammingBook"Fantasy: "#/components/schemas/FantasyBook"Sci-fi: "#/components/schemas/SciFiBook"examples:example:summary: Example bookvalue:id: 3title: New Sci-Fi Bookdescription: A new Sci-Fi book descriptionprice: 1999category: Sci-fiauthor:name: New Authorphoto: https://example.com/photos/newauthor.jpgbiography: New Author is an upcoming writer in the Sci-Fi genre...cover_image: https://example.com/covers/newbook.jpg/books/{bookId}:get:summary: Get a book by IDoperationId: getBookByIddescription: Returns a single booksecurity: []tags:- Booksparameters:- name: bookIddescription: ID of the book to returnin: pathrequired: trueschema:type: integerexample: 1responses:"200":description: A single bookcontent:application/json:schema:oneOf:- $ref: "#/components/schemas/ProgrammingBook"- $ref: "#/components/schemas/FantasyBook"- $ref: "#/components/schemas/SciFiBook"discriminator:propertyName: categorymapping:Programming: "#/components/schemas/ProgrammingBook"Fantasy: "#/components/schemas/FantasyBook"Sci-fi: "#/components/schemas/SciFiBook"examples:example1:summary: Programming book examplevalue:id: 1title: Clean Codedescription: A Handbook of Agile Software Craftsmanshipprice: 2999category: Programmingauthor:id: 1name: Robert C. Martinphoto: https://example.com/photos/robert.jpgbiography: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...cover_image: https://example.com/covers/cleancode.jpgexample2:summary: Fantasy book examplevalue:id: 2title: The Hobbitdescription: A fantasy novel by J.R.R. Tolkienprice: 1599category: Fantasyauthor:id: 2name: J.R.R. Tolkienphoto: https://example.com/photos/tolkien.jpgbiography: John Ronald Reuel Tolkien was an English writer, poet, philologist, and academic...cover_image: https://example.com/covers/thehobbit.jpg/books/{bookId}/cover:put:summary: Update a book cover by IDoperationId: updateBookCoverByIddescription: Updates a single book coversecurity:- apiKey: []tags:- Booksparameters:- name: bookIddescription: ID of the book to updatein: pathrequired: trueschema:type: integerexample: 1requestBody:description: Book coverrequired: truecontent:multipart/form-data:schema:type: objectproperties:cover:type: stringformat: binaryresponses:"200":description: Book cover updated successfully/orders:get:summary: Get all ordersoperationId: getAllOrdersdescription: Returns a list of orderstags:- Orderssecurity:- clientCredentials:- orders.readresponses:"200":description: A list of orderscontent:application/json:schema:type: arrayitems:$ref: "#/components/schemas/Order"examples:example:summary: Example responsevalue:- id: 1date: 2023-05-17T09:24:00Zstatus: pendinguser:id: 1email: test@example.comname: John Doeproducts:- id: 1title: Clean Codedescription: A Handbook of Agile Software Craftsmanshipprice: 2999category: Programmingauthor:id: 1name: Robert C. Martinphoto: https://example.com/photos/robert.jpgbiography: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...cover_image: https://example.com/covers/cleancode.jpgpost:summary: Create a new orderoperationId: createOrderdescription: Creates a new ordertags:- Orderssecurity:- clientCredentials:- orders.writerequestBody:description: Order object to be createdrequired: truecontent:application/json:schema:$ref: "#/components/schemas/NewOrder"examples:example:summary: Example ordervalue:user: 1products:- 1- 3status: pendingresponses:"201":description: Order created successfullycontent:application/json:schema:$ref: "#/components/schemas/Order"/orders/{orderId}:get:summary: Get an order by IDoperationId: getOrderByIddescription: Returns a single ordertags:- Orderssecurity:- clientCredentials:- orders.readparameters:- name: orderIddescription: ID of the order to returnin: pathrequired: trueschema:type: integerexample: 1responses:"200":description: A single ordercontent:application/json:schema:$ref: "#/components/schemas/Order"examples:example:summary: Example responsevalue:id: 1date: 2023-05-17T09:24:00Zstatus: pendinguser:id: 1email: user@example.comproducts:- id: 1title: Clean Codedescription: A Handbook of Agile Software Craftsmanshipprice: 2999category: Programmingauthor:id: 1name: Robert C. Martinphoto: https://example.com/photos/robert.jpgbiography: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...cover_image: https://example.com/covers/cleancode.jpg/orderstream:get:summary: Get a stream of ordersoperationId: getOrderStreamdescription: Returns a stream of orderstags:- Orderssecurity:- apiKey: []responses:"200":description: A stream of orderscontent:text/event-stream:schema:$ref: "#/components/schemas/OrderStreamMessage"components:schemas:ProductId:type: integerexample: 1Book:type: objectrequired:- title- description- price- category- authorproperties:id:$ref: "#/components/schemas/ProductId"title:type: stringexample: Clean Codedescription:type: stringexample: A Handbook of Agile Software Craftsmanshipprice:type: integerdescription: Price in USD centsexample: 2999category:type: stringenum:- Sci-fi- Fantasy- Programmingexample: Programmingauthor:$ref: "#/components/schemas/Author"cover_image:type: stringexample: https://example.com/covers/cleancode.jpgexample:id: 1title: Clean Codedescription: A Handbook of Agile Software Craftsmanshipprice: 2999author:id: 1name: Robert C. Martinphoto: https://example.com/photos/robert.jpgbiography: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...category: ProgrammingSciFiBook:allOf:- $ref: "#/components/schemas/Book"- type: objectproperties:category:type: stringconst: Sci-fiexample: Sci-fiexample:id: 3title: New Sci-Fi Bookdescription: A new Sci-Fi book descriptionprice: 1999category: Sci-ficover_image: https://example.com/covers/newbook.jpgauthor:name: New Authorphoto: https://example.com/photos/newauthor.jpgbiography: New Author is an upcoming writer in the Sci-Fi genre...FantasyBook:allOf:- $ref: "#/components/schemas/Book"- type: objectproperties:category:type: stringconst: Fantasyexample: Fantasyexample:id: 2title: The Hobbitdescription: A fantasy novel by J.R.R. Tolkienprice: 1599category: FantasyProgrammingBook:allOf:- $ref: "#/components/schemas/Book"- type: objectproperties:category:type: stringconst: Programmingexample: Programmingexample:id: 1title: Clean Codedescription: A Handbook of Agile Software Craftsmanshipprice: 2999category: ProgrammingAuthorId:type: integerexample: 1Author:type: objectproperties:id:$ref: "#/components/schemas/AuthorId"name:type: stringexample: Robert C. Martinphoto:type: stringexample: https://example.com/photos/robert.jpgbiography:type: stringexample: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...anyOf:- required:- nametitle: Author with name- required:- idtitle: Author with IDexample:id: 1name: Robert C. Martinphoto: https://example.com/photos/robert.jpgbiography: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...OrderId:type: integerexample: 1Order:type: objectrequired:- id- date- status- user- productsproperties:id:$ref: "#/components/schemas/OrderId"date:type: stringformat: date-timeexample: 2023-05-17T09:24:00Zstatus:type: stringenum:- pending- shipped- deliveredexample: pendinguser:$ref: "#/components/schemas/User"products:type: arrayitems:oneOf:- $ref: "#/components/schemas/FantasyBook"- $ref: "#/components/schemas/ProgrammingBook"- $ref: "#/components/schemas/SciFiBook"discriminator:propertyName: categorymapping:Programming: "#/components/schemas/ProgrammingBook"Fantasy: "#/components/schemas/FantasyBook"Sci-fi: "#/components/schemas/SciFiBook"example:id: 1date: 2023-05-17T09:24:00Zstatus: pendinguser:id: 1email: user@example.comname: John Doeproducts:- id: 1title: Clean Codedescription: A Handbook of Agile Software Craftsmanshipprice: 2999category: Programmingauthor:id: 1name: Robert C. Martinphoto: https://example.com/photos/robert.jpgbiography: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...- id: 2title: The Hobbitdescription: A fantasy novel by J.R.R. Tolkienprice: 1599category: Fantasyauthor:id: 2name: J.R.R. Tolkienphoto: https://example.com/photos/tolkien.jpgbiography: John Ronald Reuel Tolkien was an English writer, poet, philologist, and academic...OrderStreamMessage:type: objectdescription: A message in the order streamrequired:- id- event- dataproperties:id:type: stringtitle: Message IDformat: UUIDexample: 123e4567-e89b-12d3-a456-426614174000event:title: Event typetype: stringexample: order_createddata:$ref: "#/components/schemas/Order"NewOrder:type: objectrequired:- user- products- statusproperties:user:$ref: "#/components/schemas/UserId"products:type: arrayitems:$ref: "#/components/schemas/ProductId"example:user: 1products:- 1- 3- 5status: pendingUserId:type: integerexample: 1User:type: objectproperties:id:$ref: "#/components/schemas/UserId"email:type: stringexample: user@example.comname:type: stringexample: John Doeexample:id: 1email: user@example.comname: John DoesecuritySchemes:apiKey:type: apiKeyin: headername: X-API-KeyclientCredentials:type: oauth2flows:clientCredentials:tokenUrl: https://api.bookstore.com/oauth/tokenrefreshUrl: https://api.bookstore.com/oauth/refreshscopes: {}
We’ll save this as openapi.yaml
in the root of our test repository.
Installing the APIMatic CLI
The APIMatic CLI depends on Node.js, and we’ll install it using npm. In the terminal, run:
npm install -g @apimatic/cli
This will install the APIMatic CLI in your global node_modules
folder and create the apimatic
command.
On our test environment, this installed 250 npm packages, 17 of which were deprecated.
Check your APIMatic CLI version:
apimatic --version# @apimatic/cli/1.1.0-alpha.5 darwin-arm64 node-v20.17.0
Authenticate with APIMatic by running:
apimatic auth:login
Then enter your APIMatic email address and password.
The APIMatic CLI is also open source (opens in a new tab), with the latest update in September 2023.
Installing the Speakeasy CLI
To install the Speakeasy CLI, we’ll follow the Speakeasy Getting Started guide.
-
Create an account on Speakeasy (opens in a new tab).
-
Install the Speakeasy CLI using Homebrew or cURL:
brew install speakeasy-api/tap/speakeasyor
curl -fsSL https://go.speakeasy.com/cli-install.sh | sh -
Authenticate the CLI with Speakeasy:
speakeasy auth login
You can check the Speakeasy version:
speakeasy --version# speakeasy version 1.390.5# darwin_arm64
Linting OpenAPI Documents
Both Speakeasy and APIMatic can validate OpenAPI documents.
Validate openapi.yaml
using APIMatic:
apimatic api:validate --file=openapi.yaml# Validating specification file... done# Info: One or more elements in the API specification has a missing description. (View Details)Source: API.# ...# Specification file provided is valid
Speakeasy goes beyond validation by linting an OpenAPI document, and then providing separate errors, warnings, and hints. This includes a link to a shareable lint report for easier collaboration.
speakeasy lint openapi -s openapi.yaml
Both platforms validated our OpenAPI document without errors, so let’s move on to generating SDKs.
Generating an SDK Using the APIMatic CLI Tutorial
We’ll follow the APIMatic tutorial (opens in a new tab) to generate a TypeScript SDK.
In the terminal, run:
apimatic sdk:generate --platform=typescript --file="openapi.yaml"
This should print the following to the terminal:
Generating SDK... doneDownloading SDK... doneSuccess! Your SDK is located at ~/speakeasy-apimatic-comparison/openapi_sdk_typescript
Inspecting the APIMatic-Generated SDK
To see what was generated, run the tree command from the new SDK directory:
tree openapi_sdk_typescript
- LICENSE
- README.md
- jest.config.js
- package.json
- tsconfig.base.json
- tsconfig.cjs.json
- tsconfig.esm.json
- tsconfig.json
Poking around the source, we found that the generated SDK did not contain any model code or types related to our bookstore example.
Corrupted SDK Generation
In a previous test from January 2024, we used the standard OpenAPI Petstore example API to generate an SDK using APIMatic. In that case, the SDK also did not contain any models, and we encountered a bizarre error - the src/clientInterface.ts
file seems to have been generated incorrectly. Here’s what we found:
SdkRequestBuilderFactory = RequestBuilderFactory<Server,AuthParams>;export type SdkRequestBuilder = ReturnType<SdkRequestBuilderFactory>;export type Server = 'default';export type AuthParams = boolean;/*** Swagger PetstoreLib** This file was automatically generated by APIMATIC v3.0 ( https://www.apimatic.io ).*/import { RequestBuilderFactory } from './core';export interface ClientInterface {getRequestBuilderFactory(): SdkRequestBuilderFactory;}export type
The file seems to start in the middle, then wraps around.
This error gave us an opportunity to engage with the APIMatic support team, so we’ll take a brief detour and share our experience: After seven days of filing the bug report, we received a curt, “We’ve tested out the SDKs via the CLI method and they’re being generated as expected.” To be fair, the support agent did offer to provide further assistance if we still had a problem.
As any responsible tester would do, we decided to isolate the test environment from our system to be sure the issue didn’t stem from an error on our side. We’re using nvm on macOS after all, and issues can crop up when switching between Node versions.
We created a Dockerfile to install the requirements and generate an SDK:
FROM node:latestARG apimatic_auth_key=APIMATIC_KEYRUN mkdir -p /usr/src/appWORKDIR /usr/src/appCOPY petstore.yaml /usr/src/appRUN npm install -g @apimatic/cliRUN apimatic auth:login --auth-key=$apimatic_auth_keyRUN apimatic sdk:generate --platform=typescript --file="petstore.yaml"RUN cat petstore_sdk_typescript/src/clientInterface.ts
Replace $APIMATIC_KEY
with your APIMatic API key, then run:
docker build -t apimatic-petstore --build-arg apimatic_auth_key=$APIMATIC_KEY --progress plain .
On a second run with Docker, the bug appeared to fix itself, only to fail again later.
Sure enough, the result was the same - the src/clientInterface.ts
file starts in the middle and wraps around. This might be caused by a race condition in the code that downloads and unzips the SDK from APIMatic. Tempting as it is to track this error down, we have SDKs to generate, so we’ll move on.
If you’re following along, remember to delete your Docker image, as it contains your API key. The key also appears in your Docker history.
In the terminal, run:
docker image rm apimatic-petstore
In both our Petstore and Bookstore examples, the APIMatic CLI failed to generate a usable SDK.
We need to find another way to generate an SDK using APIMatic - let’s try the web app.
Generating an SDK Using the APIMatic UI
Log in to the APIMatic web application and follow the prompts to import our openapi.yaml
document as a new API.
Click on Generate and select TypeScript. This generates a TypeScript SDK, which downloads to your computer as a zip file. We’ll save this in our working directory as bookstore-sdk-apimatic
.
Here’s what’s inside:
- LICENSE
- README.md
- jest.config.js
- package.json
- tsconfig.base.json
- tsconfig.cjs.json
- tsconfig.esm.json
- tsconfig.json
This looks more complete, and none of the files are corrupted, so we’ll move on to trying the Speakeasy generator.
Create an SDK Using the Speakeasy CLI
To generate an SDK using Speakeasy, run the following in the terminal:
speakeasy generate sdk \--schema openapi.yaml \--lang typescript \--out ./bookstore-sdk-speakeasy
Speakeasy lints the OpenAPI document, then creates a new folder, bookstore-sdk-speakeasy
, with the generated SDK.
Setting Up a Mock Server
We used Stoplight Prism (opens in a new tab) to generate a mock server to test our SDKs:
npm install -g @stoplight/prism-cliprism mock openapi.yaml
This command starts a mock server at http://localhost:4010
.
SDK Code Comparison
Now that we have two SDKs, let’s compare the code generated by each platform.
Runtime Type Checking
Speakeasy creates SDKs that are type-safe from development to production. As our CEO recently wrote, Type Safe is better than Type Faith.
Speakeasy uses Zod (opens in a new tab) to validate data at runtime. Data sent to the server and data received from the server are validated against Zod definitions in the client.
This provides safer runtime code execution and helps developers who use your SDK to provide early feedback about data entered by their end users. Furthermore, trusting data validation on the client side allows developers more confidence to build optimistic UIs (opens in a new tab) that update as soon as an end user enters data, greatly improving end users’ perception of your API’s speed.
Let’s see how Speakeasy’s runtime type checking works in an example.
Consider the following Book
component from our OpenAPI document:
Book:type: objectrequired:- title- description- price- category- authorproperties:id:$ref: "#/components/schemas/ProductId"title:type: stringexample: Clean Codedescription:type: stringexample: A Handbook of Agile Software Craftsmanshipprice:type: integerdescription: Price in USD centsexample: 2999category:type: stringenum:- Sci-fi- Fantasy- Programmingexample: Programming
The highlighted price
field above has the type integer
.
// techbooks-speakeasy SDK created by Speakeasyimport { TechBooks } from "techbooks-speakeasy";const bookStore = new TechBooks({apiKey: "123",});async function run() {await bookStore.books.addBook({author: {name: "Robert C. Martin",photo: "https://example.com/photos/robert.jpg",biography: 'Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...',},category: "Programming",description: "A Handbook of Agile Software Craftsmanship",price: 29.99,title: "Clean Code",});}run();
The price
field in the Book
object in our test code is set to 29.99
, which is a floating-point number. This will cause a validation error before the data is sent to the server, as the price
field is expected to be an integer.
Handling Zod validation errors (opens in a new tab) is straightforward, and allows developers to provide meaningful feedback to their end users early in the process.
The same book object in code using the SDK generated by APIMatic will only be validated on the server. This means that the error will only be caught from the client’s perspective after the data is sent to the server, and the server responds with an error message.
If the server is not set up to validate the price
field, the error will not be caught at all, leading to unexpected behavior in your developer-users’ applications.
As a result, developers using the SDK generated by APIMatic may need to write additional client-side validation code to catch these errors before they are sent to the server.
Dependency Injection: SDK Hooks
Speakeasy generates a clean mechanism for safely injecting custom code.
The abridged code below is from the SDK generated by Speakeasy:
/** Code generated by Speakeasy (https://speakeasyapi.dev). DO NOT EDIT.*/// ...export interface BeforeCreateRequestHook {/*** A hook that is called before the SDK creates a `Request` object. The hook* can modify how a request is constructed since certain modifications, like* changing the request URL, cannot be done on a request object directly.*/beforeCreateRequest: (hookCtx: BeforeCreateRequestContext, input: RequestInput) => RequestInput;}export interface BeforeRequestHook {/*** A hook that is called before the SDK sends a request. The hook can* introduce instrumentation code such as logging, tracing and metrics or* replace the request before it is sent or throw an error to stop the* request from being sent.*/beforeRequest: (hookCtx: BeforeRequestContext, request: Request) => Awaitable<Request>;}// ...
The types above are well documented, but you can read more about Speakeasy SDK Hooks in Speakeasy’s documentation.
In short, hooks are typed and contain relevant context depending on when in the lifecycle they are applied. To add hooks, register hooks in the src/hooks/registration.ts
file in your TypeScript SDK.
Here’s an example hook:
import { Hooks } from "./types";/** This file is only ever generated once on the first generation and then is free to be modified.* Any hooks you wish to add should be registered in the initHooks function. Feel free to define them* in this file or in separate files in the hooks folder.*/export function initHooks(hooks: Hooks) {// Add hooks by calling hooks.register{ClientInit/BeforeCreateRequest/BeforeRequest/AfterSuccess/AfterError}Hook// with an instance of a hook that implements that specific Hook interface// Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instancehooks.registerBeforeCreateRequestHook({beforeCreateRequest: (hookCtx, input) => {// Modify the request input hereconsole.log("BeforeCreateRequestHook", input);console.log("HookContext", hookCtx);return input;},});}
Speakeasy also provides a clean abstraction to add dependencies to the SDK, by specifying dependencies in the SDK’s gen.yaml file:
typescript:additionalDependencies:dependencies:uuid: ^9.0.1devDependencies:"@types/uuid": "^9.0.8"peerDependencies: {}
Dependency injection and SDK customization are not well documented for APIMatic. Of course, developers can patch the generated SDKs as much as they want, but mixing generated and custom code is often a recipe for disaster.
OAuth Client Credentials Handling
Both SDKs handle OAuth 2.0 with client credentials.
Our bookstore API requires an OAuth 2.0 token with client credentials to access the API. Let’s see how the SDKs handle this.
Consider the following OAuth 2.0 configuration from our OpenAPI document:
clientCredentials:type: oauth2flows:clientCredentials:tokenUrl: https://api.bookstore.com/oauth/tokenrefreshUrl: https://api.bookstore.com/oauth/refreshscopes: {}
The SDK generated by Speakeasy takes a clientID
and clientSecret
when instantiating the SDK. The SDK also includes ClientCredentialsHook
class that implements BeforeRequestHook
to check whether the token is expired and refresh it if necessary. The hook also checks whether the client has the necessary scopes to access the endpoint, and handles authentication errors.
// techbooks-speakeasy SDK created by Speakeasyimport { TechBooks } from "techbooks-speakeasy";const bookStore = new TechBooks({security: {// OAuth 2.0 client credentialsclientID: "<YOUR_CLIENT_ID_HERE>",clientSecret: "<YOUR_CLIENT_SECRET_HERE>",},});async function run() {// The SDK handles the token lifecycle, retries, and error handling for youawait bookStore.books.addBook({// Book object});}run();
The SDK generated by APIMatic has similar functionality.
Server-Sent Events (SSE) and Streaming Responses
Our bookstore API includes an operation that streams orders to the client using Server-Sent Events (SSE).
paths:/orderstream:get:summary: Get a stream of ordersoperationId: getOrderStreamdescription: Returns a stream of orderstags:- Orderssecurity:- apiKey: []responses:"200":description: A stream of orderscontent:text/event-stream:schema:$ref: "#/components/schemas/OrderStreamMessage"
Let’s see how the SDKs handle this.
Speakeasy generates types and methods for handling SSE without any customization. Here’s an example of how to use the SDK to listen for new orders:
import { TechBooks } from "techbooks-speakeasy";const bookStore = new TechBooks({apiKey: 'KEY123',});async function run() {const result = await bookStore.orders.getOrderStream();if (result.orderStreamMessage == null) {throw new Error('Failed to create stream: received null value');}const stream = result.orderStreamMessage.stream;if (!stream || typeof stream.getReader !== 'function') {throw new Error('Invalid stream: expected a ReadableStream');}const reader = stream.getReader();try {while (true) {const { done, value } = await reader.read();if (done) break;console.log(new TextDecoder().decode(value));}} catch (error) {console.error('Error reading stream', error);} finally {reader.releaseLock();}}run();
(The example above does not run against a local Prism server, but you can test it against Stoplight’s hosted Prism (opens in a new tab) server.)
APIMatic does not generate SSE-handling code.
Discriminated Unions
Our OpenAPI document includes a Book
component with a category
field that can be one of three values: Programming
, Fantasy
, or SciFi
.
This allows us to type the Book
component in requests and responses as specific book types, such as ProgrammingBook
, FantasyBook
, and SciFiBook
.
OpenAPI supports discriminated unions using the discriminator
field in the schema. Here’s an example of a response that returns an array of books of different types:
schema:type: arrayitems:oneOf:- $ref: "#/components/schemas/ProgrammingBook"- $ref: "#/components/schemas/FantasyBook"- $ref: "#/components/schemas/SciFiBook"discriminator:propertyName: categorymapping:Programming: "#/components/schemas/ProgrammingBook"Fantasy: "#/components/schemas/FantasyBook"Sci-fi: "#/components/schemas/SciFiBook"
Let’s see how the SDKs handle this.
Speakeasy generates TypeScript types for each book type, and uses a discriminated union to handle the different book types. This enables developers to use the correct type when working with books of different categories. This pattern could just as easily apply to payment methods or delivery options.
The example below shows how Speakeasy defines the ProgrammingBook
type. It also generates types for FantasyBook
and SciFiBook
.
In this example, you’ll notice that the category
field is optional in the ProgrammingBook
type, but is enforced by Zod validation in the SDK.
/** Code generated by Speakeasy (https://speakeasyapi.dev). DO NOT EDIT.*/import { Author, Author$ } from "./author";import * as z from "zod";export type ProgrammingBook = {id?: number | undefined;title: string;description: string;/*** Price in USD cents*/price: number;category?: "Programming" | undefined;author: Author;coverImage?: string | undefined;};/** @internal */export namespace ProgrammingBook$ {export const inboundSchema: z.ZodType<ProgrammingBook, z.ZodTypeDef, unknown> = z.object({id: z.number().int().optional(),title: z.string(),description: z.string(),price: z.number().int(),category: z.literal("Programming").optional(),author: Author$.inboundSchema,cover_image: z.string().optional(),}).transform((v) => {return {...(v.id === undefined ? null : { id: v.id }),title: v.title,description: v.description,price: v.price,...(v.category === undefined ? null : { category: v.category }),author: v.author,...(v.cover_image === undefined ? null : { coverImage: v.cover_image }),};});export type Outbound = {id?: number | undefined;title: string;description: string;price: number;category: "Programming";author: Author$.Outbound;cover_image?: string | undefined;};export const outboundSchema: z.ZodType<Outbound, z.ZodTypeDef, ProgrammingBook> = z.object({id: z.number().int().optional(),title: z.string(),description: z.string(),price: z.number().int(),category: z.literal("Programming").default("Programming" as const),author: Author$.outboundSchema,coverImage: z.string().optional(),}).transform((v) => {return {...(v.id === undefined ? null : { id: v.id }),title: v.title,description: v.description,price: v.price,category: v.category,author: v.author,...(v.coverImage === undefined ? null : { cover_image: v.coverImage }),};});}
We can see how Speakeasy generates SDK code to handle the different book types in the response for the getgetAllBooks
operation:
/** Code generated by Speakeasy (https://speakeasyapi.dev). DO NOT EDIT.*/import * as components from "../components";import * as z from "zod";export type ResponseBody =| (components.ProgrammingBook & { category: "Programming" })| (components.FantasyBook & { category: "Fantasy" })| (components.SciFiBook & { category: "Sci-fi" });export type GetAllBooksResponse = {httpMeta: components.HTTPMetadata;/*** A list of books*/responseBodies?:| Array<| (components.ProgrammingBook & { category: "Programming" })| (components.FantasyBook & { category: "Fantasy" })| (components.SciFiBook & { category: "Sci-fi" })>| undefined;};// ...
Note how the array elements in responseBodies
are typed according to the book category.
This may seem like a trivial example, but it illustrates how Speakeasy generates types that are more specific and easier to work with than the types generated by APIMatic. This could, for instance, help developers correctly handle different book types in their applications.
APIMatic does not generate types for discriminated unions, and developers must manually handle the different book types in the response.
Here is the equivalent type definition generated by APIMatic:
/*** Bookstore APILib** This file was automatically generated by APIMATIC v3.0 ( https://www.apimatic.io ).*/// ...import { CategoryEnum, categoryEnumSchema } from './categoryEnum';// ...export interface ProgrammingBook {id?: number;title: string;description: string;/** Price in USD cents */price: number;category: CategoryEnum;author: Author2;coverImage?: string;}// ...
Following the CategoryEnum
import:
/*** Bookstore APILib** This file was automatically generated by APIMATIC v3.0 ( https://www.apimatic.io ).*/// .../*** Enum for CategoryEnum*/export enum CategoryEnum {Scifi = 'Sci-fi',Fantasy = 'Fantasy',Programming = 'Programming',}// ...
Discriminating between different book types in the response is left to users.
OpenAPI Overlays
If editing your OpenAPI document is not an option, Speakeasy also supports the OpenAPI Overlays specification, which allows you to add or override parts of an OpenAPI document without modifying the original document.
This step can form part of your CI/CD pipeline, ensuring that your SDKs are always up-to-date with your API, even if your OpenAPI document is generated from code.
Speakeasy’s CLI can also generate OpenAPI overlays for you, based on the differences between two OpenAPI documents.
SDK and Bundle Size
Let’s compare the bundle sizes of the SDKs generated by Speakeasy and APIMatic.
Start by adding a sdk-tests/speakeasy.ts
file that imports the Speakeasy SDK:
import { SDKCore } from "openapi/core.js";import { booksAddBook } from "openapi/funcs/booksAddBook.js";// Use `SDKCore` for best tree-shaking performance.// You can create one instance of it to use across an application.const sdk = new SDKCore({apiKey: "<YOUR_API_KEY_HERE>",});async function run() {const res = await booksAddBook(sdk, {id: 1,title: "New Sci-Fi Book",description: "A new Sci-Fi book description",category: "Sci-fi",price: 1999,author: {id: 1,name: "New Author",photo: "https://example.com/photos/newauthor.jpg",biography: "New Author is an upcoming writer in the Sci-Fi genre...",},coverImage: "https://example.com/covers/newbook.jpg",});if (!res.ok) {throw res.error;}const { value: result } = res;// Handle the resultconsole.log(result)}run();
Next, add a sdk-tests/apimatic.ts
file that imports the APIMatic SDK:
import {Client,BooksController,AddBookBody,AddBookResponse,CategoryEnum,ApiError,} from "bookstore-apilib";const client = new Client({apiKeyCredentials: {"X-API-Key": "YOUR_API_KEY",},});const booksController = new BooksController(client);const body: AddBookBody = {title: "Clean Code",description: "A Handbook of Agile Software Craftsmanship",price: 2999,category: CategoryEnum.Programming,author: {name: "Robert C. Martin",photo: "https://example.com/photos/robert.jpg",biography:'Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...',},id: 1,coverImage: "https://example.com/covers/cleancode.jpg",};async function addBook() {try {const { result, ...httpResponse } = await booksController.addBook(body);console.log(result);if (AddBookResponse.isProgrammingBook(result)) {// Use the result narrowed down to ProgrammingBook type.} else if (AddBookResponse.isFantasyBook(result)) {// Use the result narrowed down to FantasyBook type.} else if (AddBookResponse.isSciFiBook(result)) {// Use the result narrowed down to SciFiBook type.} else {// result is narrowed down to type 'never'.}// Get more response info...// const { statusCode, headers } = httpResponse;} catch (error) {console.error(error);if (error instanceof ApiError) {const errors = error.result;}}}addBook();
Running the code above generates a validation error, due to the lack of discriminated unions in the SDK generated by APIMatic. This won’t affect the bundle size, though.
We’ll use esbuild
to bundle the SDKs. First, install esbuild
:
npm install esbuild
Next, add a sdk-tests/build.js
script that uses esbuild
to bundle the SDKs:
import * as esbuild from "esbuild";import * as fs from "fs";const speakeasyBuild = await esbuild.build({entryPoints: ["speakeasy.ts"],outfile: "dist/speakeasy.cjs",bundle: true,minify: true,treeShaking: true,metafile: true,target: "node18",platform: "node",});fs.writeFileSync("dist/speakeasy.json",JSON.stringify(speakeasyBuild.metafile, null, 2));const apimaticBuild = await esbuild.build({entryPoints: ["apimatic.ts"],outfile: "dist/apimatic.cjs",bundle: true,minify: true,treeShaking: true,metafile: true,target: "node18",platform: "node",});fs.writeFileSync("dist/apimatic.json",JSON.stringify(apimaticBuild.metafile, null, 2));
Run the build.js
script:
node build.ts
This generates two bundles, dist/speakeasy.cjs
and dist/apimatic.cjs
, along with their respective metafiles.
Bundle Size Comparison
Now that we have two bundles, let’s compare their sizes.
First, let’s look at the size of the dist/speakeasy.cjs
bundle:
du -sh dist/speakeasy.cjs# Output# 128K dist/speakeasy.cjs
Next, let’s look at the size of the dist/apimatic.cjs
bundle:
du -sh dist/apimatic.cjs# Output# 360K dist/apimatic.cjs
Despite lacking runtime data validation, the bundle built with the SDK generated by APIMatic is significantly larger than that built with the SDK generated by Speakeasy.
We can use the metafiles generated by esbuild
to analyze the bundle sizes in more detail.
Analyzing Bundle Sizes
The metafiles generated by esbuild
contain detailed information about which source files contribute to each bundle’s size, presented as a tree structure.
We used esbuild’s online bundle visualizer (opens in a new tab) to analyze the bundle sizes.
Here’s a summary of the bundle sizes:
The dist/speakeasy.cjs
bundle’s largest contributor, at 43.4%, is the Zod library used for runtime data validation. The Zod library’s tree-shaking capabilities are a work in progress, and future versions of SDKs are expected to have smaller bundle sizes.
The dist/apimatic.cjs
bundle’s largest contributor, at 37.4%, is mime-db
, a “large database of mime types and information about them” (mime-db on npm (opens in a new tab)).
Bundling for the Browser
Speakeasy SDKs are designed to work in a range of environments, including the browser. To bundle an SDK for the browser, you can use a tool like esbuild
or webpack
.
Here’s an example of how to bundle the Speakeasy SDK for the browser using esbuild
:
npx esbuild speakeasy.ts --bundle --minify --target=es2020 --platform=browser --outfile=dist/speakeasy.js
Doing the same for the APIMatic SDK generates an error, as the SDK is not designed to work in the browser out of the box.
npx esbuild apimatic.ts --bundle --minify --target=es2020 --platform=browser --outfile=dist/apimatic.js# ✘ [ERROR] Could not resolve "stream"
Linting and Change Detection
Speakeasy keeps track of changes in your OpenAPI document, and versions the SDKs it creates based on changes.
Speakeasy Compared to Open-Source Generators
If you are interested in seeing how Speakeasy stacks up against other SDK generation tools, check out our post.