Background image

API Advice

In Depth: Speakeasy vs Stainless

Nolan Sullivan

Nolan Sullivan

January 10, 2025

Featured blog post image
Info Icon

NOTE

This comparison of Speakeasy & Stainless is based on a snapshot of two developing companies as of January 2025. If you think we need to update this post, please let us know!

In this post, we’ll compare generated SDKs, as well as the underlying philosophies that guide the development of the two companies. And while we acknowledge that our views may be biased, we’ll show our work along with our conclusions so that readers can decide for themselves.

In short: How is Speakeasy different?

OpenAPI-native vs OpenAPI-compatible

Speakeasy is OpenAPI-native; Stainless is OpenAPI-compatible. Stainless is built on a custom DSL known as the Stainless config (opens in a new tab). This approach requires your team to manage an additional config file. Speakeasy has no intermediary DSL. Your OpenAPI spec is the only source of truth for SDK generation.

Being OpenAPI-native is beneficial for integration into an existing stack. Regardless of the API proxies, server-side code, or specific web framework that you’re using, you can plug in Speakeasy’s tools. Stainless is doubling down on a vertically integrated approach by building a backend TypeScript framework (opens in a new tab) which will become the foundation for their solution. Their product will work best when you buy the entire stack, Speakeasy will shine regardless of the other tools in your existing stack.

Type-safe vs type-faith

There’s more on this topic in the technical deep dive below. Speakeasy SDKs guarantee true end-to-end type safety, meaning that types are generated to validate both request and response objects defined in your API. Stainless SDKs, on the other hand, are mainly type-hinted, not guarding the API from bad inputs.

Velocity and maturity

Stainless was founded in 2021 (opens in a new tab) and has expanded its language support to seven languages. By comparison, Speakeasy launched in October 2022 and has released support for ten languages in less time. The Speakeasy platform is also broader, with support for additional generation features, like webhooks, React Hooks, and contract testing, not supported by Stainless.

Both companies are financially secure, having raised $25M+ in funding.

Platform

ProductSpeakeasyStainless
SDK generation
Terraform generation
Docs generation
Contract testing
E2E testing

SDK generation

LanguageSpeakeasyStainless
TypeScript
Python
Go
Java
Kotlin⚠ Java is Kotlin-compatible
Ruby
C#
PHP
Swift
Unity

If there’s a language you require that we don’t support, add it to our roadmap (opens in a new tab).

SDK features

Breadth matters, but so does the depth of support for each language. The table below shows the current feature support for Speakeasy and Stainless’s SDK generation.

FeatureSpeakeasyStainless
Webhooks support
React Hooks
OAuth 2.0❌ (manual)
React Hooks support❌ (manual)
Retries
Pagination
Async functions
Streaming uploads
Custom SDK naming
Union types
Discriminated union types
Server-sent events⚠ non-OpenAPI standard
Custom dependency injection

Pricing

In terms of pricing, both Speakeasy and Stainless offer free plans, as well as paid plans for startups and enterprises. The most significant pricing difference is in the enterprise plan. Existing customers indicate that Stainless’s enterprise pricing is ~20% higher than Speakeasy’s. Of course, this can vary, and we recommend reaching out to both companies for a quote.

PlanSpeakeasyStainless
Free1 free Published SDK1 free local SDK; max 50 endpoints
Startup$250/mo/SDK; max 50 endpoints$250/mo/SDK; max 50 endpoints
Business600/mo/SDK ; max 200 endpoints$2,500/mo; max 5 SDKs; 150 endpoints
EnterpriseCustomCustom

Speakeasy vs Stainless: TypeScript SDK comparison

Info Icon

NOTE

For this technical comparison, we’ll create SDKs with Speakeasy and Stainless using an example bookstore API. You can find the complete OpenAPI document in this example repository (opens in a new tab).

SDK structure

Speakeasy

  • README.md
  • RUNTIMES.md
  • USAGE.md
  • jsr.json
  • package.json
  • tsconfig.json

Stainless

  • Brewfile
  • CONTRIBUTING.md
  • LICENSE
  • README.md
  • SECURITY.md
  • api.md
  • jest.config.ts
  • package.json
  • tsc-multi.json
  • tsconfig.build.json
  • tsconfig.deno.json
  • tsconfig.dist-src.json
  • tsconfig.json
  • yarn.lock

Speakeasy maintains a clear separation of concerns. There are separate folders for models and operations, both in the documentation and in the source folder, and distinct files for each component and operation.

Stainless generates an SDK that, at a glance, looks less organized, considering the greater number of configuration files at the root of the project, no separation of models and operations, and more shims scattered throughout.

Structure might seem superficial at first, but keep in mind that SDK users form their first impressions of your SDK from the same high-level overview. Some of this may be a matter of opinion, but at Speakeasy, we aim to generate SDKs that are as organized as SDKs coded by hand.

Generated types and type safety

Both Speakeasy and Stainless generate TypeScript types, enabling developers to see errors and hints during development. However, Stainless does not generate types for complex OpenAPI component schemas.

For example, consider the following Author component from our OpenAPI document.

openapi.yaml
Author:
type: object
properties:
id:
$ref: "#/components/schemas/AuthorId"
name:
type: string
example: Robert C. Martin
photo:
type: string
example: https://example.com/photos/robert.jpg
biography:
type: string
example: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...
anyOf:
- required:
- name
title: Author with name
- required:
- id
title: Author with ID
example:
id: 1
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...

The highlighted anyOf list is of particular interest. This list states that a valid Author object must have a name or an id, or both. An author with neither a name nor an id should not validate against this schema.

Let’s take a look at the relevant types generated by each SDK generator:

speakeasy/author.ts
// ...
export type AuthorWithID = {
id: number;
Only id is required
name?: string | undefined;
photo?: string | undefined;
biography?: string | undefined;
};
export type AuthorWithName = {
id?: number | undefined;
name: string;
Only name is required
photo?: string | undefined;
biography?: string | undefined;
};
export type Author = AuthorWithName | AuthorWithID;
stainless/books.ts
// ...
export namespace FantasyBook {
export interface Author {
id?: number;
Both id and name are optional
biography?: string;
name?: string;
Both id and name are optional
photo?: string;
}
}
// Repeated for ProgrammingBook and SciFiBook

Here, we see that Speakeasy generates three types for the Author schema: AuthorWithID, AuthorWithName, and a union of these, called Author.

The equivalent type generated by Stainless is an Author interface for each book type, with both id and name marked as optional.

As a result, the following example code using the Stainless SDK will incorrectly compile without any warnings. The equivalent Speakeasy code will correctly catch the compilation error:

speakeasy-example.ts
// techbooks-speakeasy SDK created by Speakeasy
import { TechBooks } from "techbooks-speakeasy";
const bookStore = new TechBooks({
apiKey: "123",
});
async function run() {
await bookStore.books.addBook({
author: {},
Compilation error: Type '{}' is not assignable to type 'Author'.
category: "Programming",
description: "A Handbook of Agile Software Craftsmanship",
price: 2999,
title: "Clean Code",
});
}
run();
stainless-example.ts
// techbooks SDK generated by Stainless
import { Techbooks } from "techbooks";
const bookStore = new Techbooks({
apiKey: "My API Key",
clientId: "My Client ID",
clientSecret: "My Client Secret",
});
async function run() {
const params: Techbooks.BookCreateParams = {
author: {},
No compilation error, even though the author object is empty
category: "Programming",
description: "A Handbook of Agile Software Craftsmanship",
price: 2999,
title: "Clean Code",
};
const result: Techbooks.BookCreateResponse = await bookStore.books.create(
params
);
}
run();

Runtime type checking

This brings us to the next type error that should be caught: runtime type errors. Speakeasy creates SDKs that are type-safe from development to production. As our CEO wrote, type safe is better than type faith.

The SDK generated by Speakeasy uses Zod (opens in a new tab) to validate the data sent to and received from the server. This provides safer runtime code execution and helps developers who use your SDK to receive early feedback about the data their app is sending. Furthermore, data validation on the client side gives users more confidence in your API’s reliability by reducing the opportunity for unintended behavior caused by unexpected data.

To see how this works, let’s look at what happens when the price field (an integer in the Book type from our example) is populated with a float value:

Book:
type: object
required:
- title
- description
- price
- category
- author
properties:
id:
$ref: "#/components/schemas/ProductId"
title:
type: string
example: Clean Code
description:
type: string
example: A Handbook of Agile Software Craftsmanship
price:
type: integer
description: Price in USD cents
example: 2999
category:
type: string
enum:
- Sci-fi
- Fantasy
- Programming
example: Programming
speakeasy-example.ts
// techbooks-speakeasy SDK created by Speakeasy
import { 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,
Validation error: Expected integer, received float
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 trigger a Zod 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 users to get fast feedback on where they are going wrong.

The same book object in code using the Stainless-generated SDK 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 users’ applications.

As a result, developers using the SDK generated by Stainless may need to write additional client-side validation code to catch these errors before they are sent to the server.

OAuth client credentials handling

Both Speakeasy and Stainless generate SDKs that handle OAuth 2.0 with client credentials. However, only Speakeasy’s SDKs handle the token lifecycle, retries, and error handling without any additional code.

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:

openapi.yaml
clientCredentials:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://api.bookstore.com/oauth/token
refreshUrl: https://api.bookstore.com/oauth/refresh
scopes: {}

Speakeasy’s generated SDK 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.

speakeasy-example/addbook.ts
import { TechBooks } from "techbooks-speakeasy";
const bookStore = new TechBooks({
security: {
// OAuth 2.0 client credentials
clientID: "<YOUR_CLIENT_ID_HERE>",
clientSecret: "<YOUR_CLIENT_SECRET_HERE>",
},
});
async function run() {
// The SDK handles the token lifecycle, retries, and error handling for you
await bookStore.books.addBook({
// Book object
});
}
run();
stainless-example/addbook.ts
import TechBooks from 'stainless-book-sdk';
import axios from 'axios';
const clientId = '<YOUR_CLIENT_ID_HERE>';
const clientSecret = '<YOUR_CLIENT_SECRET_HERE>';
async function getOAuthToken() {
const response = await axios.post('https://api.bookstore.com/oauth/token', {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret
});
return response.data.access_token;
}
const client = new TechBooks({
retries: {
maxRetries: 3
}
});
async function run() {
// Need to manually fetch and manage token
const token = await getOAuthToken();
client.defaultHeaders = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
await client.books.addBook({
// Book object
});
}
run();

The SDK generated by Stainless requires you to manually fetch and manage the OAuth token. This means that you’ll need to write additional code to handle the token lifecycle, retries, and error handling. You’ll also need to include additional configuration in the Stainless configuration file to use Bearer tokens.

stainless-config.yaml
client_settings:
opts:
access_token:
type: string
auth:
security_scheme: BearerAuth
read_env: MY_TEAM_ACCESS_TOKEN
security:
- BearerAuth: []
security_schemes:
BearerAuth:
type: http
scheme: bearer

Speakeasy does not require any additional configuration to handle OAuth 2.0 with client credentials. The SDK itself handles the token lifecycle, retries, and error handling.

Webhooks support

Webhooks enable users to receive real-time updates from your API through HTTP callbacks in your SDK. Speakeasy and Stainless both generate SDKs that support webhooks, but Speakeasy’s SDKs provide built-in support for webhook validation, payload parsing, and delivery.

Stainless doesn’t provide out-of-the-box functionality for handling webhooks. You must implement your own logic for verifying event signatures, such as HMAC or RSA, defining event payload types, and managing retry mechanisms.

The example below shows how the techbooks-speakeasy SDK can validate and parse a book.created event, as well as the corresponding OpenAPI spec defining the webhook event.

speakeasy-example/webhook.ts
// techbooks-speakeasy SDK created by Speakeasy
import { TechBooks } from "techbooks-speakeasy";
const bookStore = new TechBooks();
async function handleWebhook(request: Request) {
const secret = "my-webhook-secret";
const res = await bookStore.webhooks.validateWebhook({ request, secret });
if (res.error) {
console.error("Webhook validation failed:", res.error);
throw new Error("Invalid webhook signature");
}
// `res.data` is strongly typed based on your OpenAPI spec
const { data, inferredType } = res;
switch (data.type) {
case "book.created":
console.log("New Book Created:", data.title);
// ...
break;
case "book.deleted":
console.log("Book Deleted:", data.title);
// ...
break;
default:
console.warn(`Unhandled event type: ${inferredType}`);
}
}
openapi.yaml
openapi: 3.1.1
paths:
...
x-speakeasy-webhooks:
security:
type: signature # a preset which signs the request body with HMAC
name: x-signature # the name of the header
encoding: base64 # encoding of the signature in the header
algorithm: hmac-sha256
webhooks:
book.created:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: string
title:
type: string
required:
- id
- title
responses:
'200':
description: Book creation event received
book.deleted:
...

You can read more about how Speakeasy handles webhooks in our webhooks release post.

React Hooks

React Hooks simplify state and data management in React apps, enabling developers to consume APIs more efficiently. Speakeasy generates built-in React Hooks using TanStack Query (opens in a new tab). These hooks provide features like intelligent caching, type safety, pagination, and integration with modern React patterns like SSR and Suspense. Stainless does not generate React Hooks.

speakeasy-example/booksView.tsx
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.

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

Summary

We’ve all experienced bad SDKs that make integration with the API harder, not easier. Speakeasy is building a generator to make poorly written, poorly maintained SDKs a thing of the past. To do so, our team has put an extraordinary level of thought into getting the details of SDK generation right. We think that the effort has earned us the position to compare favorably with any other generator.

If you are interested in seeing how Speakeasy stacks up against some of the popular open-source SDK-generation tools, check out this post.

CTA background illustrations

Speakeasy Changelog

Subscribe to stay up-to-date on Speakeasy news and feature releases.