# speakeasy-vs-liblab
Source: https://speakeasy.com/blog/speakeasy-vs-liblab


import { Table } from "@/mdx/components";
import { FileTree } from "nextra/components";

This analysis compares Speakeasy and liblab in terms of SDK generation, OpenAPI integration, language support, features, platform support, enterprise support, and pricing. We'll also provide a technical walkthrough of generating SDKs using both services and compare the generated TypeScript SDKs.

## In short: How do Speakeasy and liblab differ?

1. **OpenAPI integration:** Speakeasy is [built for OpenAPI](/docs/openapi/openapi-support), supporting advanced features of OpenAPI 3.0 and 3.1, while testing and implementing upcoming OpenAPI features like OpenAPI [Overlays](/openapi/overlays) and [Workflows](/openapi/arazzo). liblab uses OpenAPI as a starting point, but has not yet implemented advanced OpenAPI features like Overlays or Workflows, instead depending on its own configuration system. This can lead to a divergence between the OpenAPI document and the generated SDK, and may require additional configuration after spec changes.

2. **Velocity and language support**: liblab was founded in January 2022 and gradually expanded its language support and features to support seven languages. In comparison, Speakeasy was founded in May 2022, found market traction in early 2023, and released support for ten languages within 12 months. Speakeasy meets the diverse needs of users, while supporting their existing stacks.

3. **SDK generator maturity:** Speakeasy creates SDKs that are [idiomatic to each target language](/docs/sdk-design/intro), type safe during development and production, human-readable, and fault-tolerant. Our comparison found some room for improvement in liblab's type safety, fault tolerance, and SDK project structure. Both products are under active development, and improvement should be expected.

## Comparing Speakeasy and liblab

We'll start by comparing the two services in terms of their SDK generation targets, features, platform support, enterprise support, and pricing.

### SDK generation targets

As your user base grows, the diversity of their technical requirements will expand beyond the proficiencies of your team. At Speakeasy, we understand the importance of enabling users to onboard with our clients without making major changes to their tech stacks. Our solution is to offer support for a wide range of SDK generation targets.

The table below highlights the current SDK language support offered by Speakeasy and liblab as of June 2024. Please note that these lists are subject to change, so always refer to the official documentation for the most up-to-date information.

<Table
  title="SDK Generation Targets"
  columns={[
    { key: "language", header: "Language" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "liblab", header: "liblab" },
  ]}
  data={[
    { language: "Go", speakeasy: "✅", liblab: "✅" },
    { language: "Python", speakeasy: "✅", liblab: "✅" },
    { language: "Typescript", speakeasy: "✅", liblab: "✅" },
    { language: "Java", speakeasy: "✅", liblab: "✅" },
    { language: "C#", speakeasy: "✅", liblab: "✅" },
    { language: "PHP", speakeasy: "✅", liblab: "✅" },
    {
      language: "Kotlin",
      speakeasy: "⚠ Java is Kotlin-compatible",
      liblab: "⚠ Java is Kotlin-compatible",
    },
    { language: "Terraform provider", speakeasy: "✅", liblab: "✅" },
    { language: "Ruby", speakeasy: "✅", liblab: "❌" },
    { language: "Unity", speakeasy: "✅", liblab: "❌" },
    { language: "Postman Collection", speakeasy: "✅", liblab: "❌" },
  ]}
  rowKeyField="language"
/>

Everyone has that one odd language that is critical to their business. In our first year, we've made a dent, but we've got further to go. See a language that you require that we don't support? [Join our Slack community](https://go.speakeasy.com/slack) to let us know.

### SDK features

This table shows the current feature support for Speakeasy and liblab as of June 2024. Refer to the official documentation for the most recent updates.

<Table
  title="SDK Features"
  columns={[
    { key: "feature", header: "Feature" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "liblab", header: "liblab" },
  ]}
  data={[
    { feature: "Union types", speakeasy: "✅", liblab: "✅" },
    { feature: "Discriminated union types", speakeasy: "✅", liblab: "✅" },
    { feature: "Server-sent events", speakeasy: "✅", liblab: "❌" },
    { feature: "Retries", speakeasy: "✅", liblab: "⚠ Global retries only" },
    { feature: "Pagination", speakeasy: "✅", liblab: "❌" },
    { feature: "Async support", speakeasy: "✅", liblab: "✅" },
    { feature: "Streaming uploads", speakeasy: "✅", liblab: "❌" },
    { feature: "OAuth 2.0", speakeasy: "✅", liblab: "❌" },
    { feature: "Custom SDK naming", speakeasy: "✅", liblab: "✅" },
    { feature: "Customize SDK structure", speakeasy: "✅", liblab: "❌" },
    { feature: "Custom dependency injection", speakeasy: "✅", liblab: "✅" },
  ]}
  rowKeyField="feature"
/>

Speakeasy creates SDKs that handle advanced authentication. For example, Speakeasy can generate SDKs that handle OAuth 2.0 with client credentials - handling the token lifecycle, retries, and error handling for you.

liblab leaves some of these features to be implemented by the user. For example, liblab's configuration allows for global retries on all operations, but only recently released support for custom retries requiring custom implementation per SDK.

liblab also lacks support for pagination, server-sent events, and streaming uploads.

### Platform features

Speakeasy is designed to be used locally, with a dependency-free CLI that allows for local experimentation and iteration. This makes it easier to test and iterate on your SDKs and allows for custom CI/CD workflows.

For local use, liblab provides an NPM package with a dependency tree of 749 modules. Installing and updating the liblab CLI is slower than Speakeasy's single binary, the CLI is less feature-rich, and it depends on Node.js and NPM.

<Table
  title="Platform Features"
  columns={[
    { key: "feature", header: "Feature" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "liblab", header: "liblab" },
  ]}
  data={[
    { feature: "GitHub CI/CD", speakeasy: "✅", liblab: "✅" },
    { feature: "CLI", speakeasy: "✅", liblab: "✅" },
    { feature: "Web interface", speakeasy: "✅", liblab: "✅" },
    { feature: "OpenAPI Overlays", speakeasy: "✅", liblab: "❌" },
    { feature: "Package publishing", speakeasy: "✅", liblab: "✅" },
    { feature: "Product documentation", speakeasy: "✅", liblab: "✅" },
    { feature: "OpenAPI linting", speakeasy: "✅", liblab: "✅" },
    { feature: "Change detection", speakeasy: "✅", liblab: "❌" },
  ]}
  rowKeyField="feature"
/>

### Enterprise support

Both Speakeasy and liblab offer support for Enterprise customers. This includes features like concierge onboarding, private Slack channels, and enterprise SLAs.

<Table
  title="Enterprise Support"
  columns={[
    { key: "feature", header: "Feature" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "liblab", header: "liblab" },
  ]}
  data={[
    { feature: "Concierge onboarding", speakeasy: "✅", liblab: "✅" },
    { feature: "Private Slack channel", speakeasy: "✅", liblab: "✅" },
    { feature: "Enterprise SLAs", speakeasy: "✅", liblab: "✅" },
    { feature: "User issues triage", speakeasy: "✅", liblab: "✅" },
    { feature: "SOC 2 compliance", speakeasy: "❌", liblab: "✅" },
  ]}
  rowKeyField="feature"
/>

### Pricing

Speakeasy offers a free tier, with paid plans starting at $600 per month.

liblab's free tier is limited to open-source projects, with paid plans starting at $300 per month.

Both services offer custom enterprise plans.

<Table
  title="Pricing"
  columns={[
    { key: "plan", header: "Plan" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "liblab", header: "liblab" },
  ]}
  data={[
    {
      plan: "Free",
      speakeasy: "1 target; max 250 endpoints",
      liblab: "Open-source projects only",
    },
    {
      plan: "Startup/Starter",
      speakeasy: "N/A",
      liblab: "$300/mo",
    },
    {
      plan: "Business",
      speakeasy: "$600/mo/SDK; max 250 endpoints",
      liblab: "N/A",
    },
    { plan: "Enterprise/Advanced", speakeasy: "Custom", liblab: "Custom" },
  ]}
  rowKeyField="plan"
/>

## TypeScript SDK comparison

Now that we have two TypeScript SDKs generated from a single OpenAPI document, let's see how they differ.

### SDK structure overview

Before we dive into the detail, let's get an overall view of the default project structure for each SDK.

Speakeasy automatically generates detailed documentation with examples for each operation and component, while liblab generates an `examples` folder with a single example.

liblab generates a `test` folder, while Speakeasy does not. We'll take a closer look at this shortly.

In the comparison below, comparing the folder structure might seem superficial at first, but keep in mind that SDK users get the same kind of high-level glance as their first impression of your SDK. 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.

<div className="md:flex gap-10">
  <div className="md:w-1/2">

#### Speakeasy SDK structure

Speakeasy generates separate folders for models and operations, both in the documentation and in the source folder. This indicates a clear separation of concerns.

We also see separate files for each component and operation, indicating modularity and separation of concerns.

    <div className="[&>div>div]:block">
      <FileTree>
        <FileTree.File name="README.md" />
        <FileTree.File name="RUNTIMES.md" />
        <FileTree.File name="USAGE.md" />
        <FileTree.Folder name="docs">
          <FileTree.Folder name="models">
            <FileTree.Folder name="components">
              <FileTree.File name="author.md" />
              <FileTree.File name="authorwithid.md" />
              <FileTree.File name="authorwithname.md" />
              <FileTree.File name="fantasybook.md" />
              <FileTree.File name="httpmetadata.md" />
              <FileTree.File name="neworder.md" />
              <FileTree.File name="order.md" />
              <FileTree.File name="orderstreammessage.md" />
              <FileTree.File name="products.md" />
              <FileTree.File name="programmingbook.md" />
              <FileTree.File name="scifibook.md" />
              <FileTree.File name="security.md" />
              <FileTree.File name="status.md" />
              <FileTree.File name="user.md" />
            </FileTree.Folder>
            <FileTree.Folder name="operations">
              <FileTree.File name="addbookrequestbody.md" />
              <FileTree.File name="addbookresponse.md" />
              <FileTree.File name="addbookresponsebody.md" />
              <FileTree.File name="cover.md" />
              <FileTree.File name="createorderresponse.md" />
              <FileTree.File name="getallbooksresponse.md" />
              <FileTree.File name="getallordersresponse.md" />
              <FileTree.File name="getbookbyidrequest.md" />
              <FileTree.File name="getbookbyidresponse.md" />
              <FileTree.File name="getbookbyidresponsebody.md" />
              <FileTree.File name="getorderbyidrequest.md" />
              <FileTree.File name="getorderbyidresponse.md" />
              <FileTree.File name="getorderstreamresponse.md" />
              <FileTree.File name="responsebody.md" />
              <FileTree.File name="updatebookcoverbyidrequest.md" />
              <FileTree.File name="updatebookcoverbyidrequestbody.md" />
              <FileTree.File name="updatebookcoverbyidresponse.md" />
            </FileTree.Folder>
          </FileTree.Folder>
          <FileTree.Folder name="sdks">
            <FileTree.Folder name="books">
              <FileTree.File name="README.md" />
            </FileTree.Folder>
            <FileTree.Folder name="orders">
              <FileTree.File name="README.md" />
            </FileTree.Folder>
            <FileTree.Folder name="sdk">
              <FileTree.File name="README.md" />
            </FileTree.Folder>
          </FileTree.Folder>
        </FileTree.Folder>
        <FileTree.File name="jsr.json" />
        <FileTree.File name="package.json" />
        <FileTree.Folder name="src">
          <FileTree.Folder name="hooks">
            <FileTree.File name="hooks.ts" />
            <FileTree.File name="index.ts" />
            <FileTree.File name="registration.ts" />
            <FileTree.File name="types.ts" />
          </FileTree.Folder>
          <FileTree.File name="index.ts" />
          <FileTree.Folder name="lib">
            <FileTree.File name="base64.ts" />
            <FileTree.File name="config.ts" />
            <FileTree.File name="encodings.ts" />
            <FileTree.File name="event-streams.ts" />
            <FileTree.File name="http.ts" />
            <FileTree.File name="primitives.ts" />
            <FileTree.File name="retries.ts" />
            <FileTree.File name="schemas.ts" />
            <FileTree.File name="sdks.ts" />
            <FileTree.File name="security.ts" />
            <FileTree.File name="url.ts" />
          </FileTree.Folder>
          <FileTree.Folder name="models">
            <FileTree.Folder name="components">
              <FileTree.File name="author.ts" />
              <FileTree.File name="fantasybook.ts" />
              <FileTree.File name="httpmetadata.ts" />
              <FileTree.File name="index.ts" />
              <FileTree.File name="neworder.ts" />
              <FileTree.File name="order.ts" />
              <FileTree.File name="orderstreammessage.ts" />
              <FileTree.File name="programmingbook.ts" />
              <FileTree.File name="scifibook.ts" />
              <FileTree.File name="security.ts" />
              <FileTree.File name="user.ts" />
            </FileTree.Folder>
            <FileTree.Folder name="errors">
              <FileTree.File name="index.ts" />
              <FileTree.File name="sdkerror.ts" />
              <FileTree.File name="sdkvalidationerror.ts" />
            </FileTree.Folder>
            <FileTree.Folder name="operations">
              <FileTree.File name="addbook.ts" />
              <FileTree.File name="createorder.ts" />
              <FileTree.File name="getallbooks.ts" />
              <FileTree.File name="getallorders.ts" />
              <FileTree.File name="getbookbyid.ts" />
              <FileTree.File name="getorderbyid.ts" />
              <FileTree.File name="getorderstream.ts" />
              <FileTree.File name="index.ts" />
              <FileTree.File name="updatebookcoverbyid.ts" />
            </FileTree.Folder>
          </FileTree.Folder>
          <FileTree.Folder name="sdk">
            <FileTree.File name="books.ts" />
            <FileTree.File name="index.ts" />
            <FileTree.File name="orders.ts" />
            <FileTree.File name="sdk.ts" />
          </FileTree.Folder>
          <FileTree.Folder name="types">
            <FileTree.File name="blobs.ts" />
            <FileTree.File name="enums.ts" />
            <FileTree.File name="index.ts" />
            <FileTree.File name="operations.ts" />
            <FileTree.File name="rfcdate.ts" />
          </FileTree.Folder>
        </FileTree.Folder>
        <FileTree.File name="tsconfig.json" />
      </FileTree>
    </div>

  </div>
  <div className="md:w-1/2">

#### liblab SDK structure

liblab generates an SDK that at a glance looks less organized, considering the greater number of configuration files at the root of the project, the lack of a `docs` folder, and the way the `src` folder is structured by OpenAPI tags instead of models and operations.

    <div className="[&>div>div]:block">
      <FileTree>
        <FileTree.Folder name="examples">
          <FileTree.File name="package.json" />
          <FileTree.File name="README.md" />
          <FileTree.Folder name="src">
            <FileTree.File name="index.ts" />
          </FileTree.Folder>
          <FileTree.File name="tsconfig.json" />
        </FileTree.Folder>
        <FileTree.File name="install.sh" />
        <FileTree.File name="jest.config.json" />
        <FileTree.File name="LICENSE" />
        <FileTree.File name="package.json" />
        <FileTree.File name="README.md" />
        <FileTree.Folder name="src">
          <FileTree.File name="BaseService.ts" />
          <FileTree.Folder name="hooks">
            <FileTree.File name="Hook.ts" />
          </FileTree.Folder>
          <FileTree.Folder name="http">
            <FileTree.File name="Environment.ts" />
            <FileTree.Folder name="errors">
              <FileTree.File name="BadGateway.ts" />
              <FileTree.File name="BadRequest.ts" />
              <FileTree.File name="base.ts" />
              <FileTree.File name="Conflict.ts" />
              <FileTree.File name="ExpectationFailed.ts" />
              <FileTree.File name="FailedDependency.ts" />
              <FileTree.File name="Forbidden.ts" />
              <FileTree.File name="GatewayTimeout.ts" />
              <FileTree.File name="Gone.ts" />
              <FileTree.File name="HttpVersionNotSupported.ts" />
              <FileTree.File name="index.ts" />
              <FileTree.File name="InternalServerError.ts" />
              <FileTree.File name="LengthRequired.ts" />
              <FileTree.File name="Locked.ts" />
              <FileTree.File name="LoopDetected.ts" />
              <FileTree.File name="MethodNotAllowed.ts" />
              <FileTree.File name="MisdirectedRequest.ts" />
              <FileTree.File name="NetworkAuthenticationRequired.ts" />
              <FileTree.File name="NotAcceptable.ts" />
              <FileTree.File name="NotExtended.ts" />
              <FileTree.File name="NotFound.ts" />
              <FileTree.File name="NotImplemented.ts" />
              <FileTree.File name="PayloadTooLarge.ts" />
              <FileTree.File name="PaymentRequired.ts" />
              <FileTree.File name="PreconditionFailed.ts" />
              <FileTree.File name="PreconditionRequired.ts" />
              <FileTree.File name="ProxyAuthenticationRequired.ts" />
              <FileTree.File name="RangeNotSatisfiable.ts" />
              <FileTree.File name="RequestHeaderFieldsTooLarge.ts" />
              <FileTree.File name="RequestTimeout.ts" />
              <FileTree.File name="ServiceUnavailable.ts" />
              <FileTree.File name="TooEarly.ts" />
              <FileTree.File name="TooManyRequests.ts" />
              <FileTree.File name="Unauthorized.ts" />
              <FileTree.File name="UnavailableForLegalReasons.ts" />
              <FileTree.File name="UnprocessableEntity.ts" />
              <FileTree.File name="UnsufficientStorage.ts" />
              <FileTree.File name="UnsupportedMediaType.ts" />
              <FileTree.File name="UpgradeRequired.ts" />
              <FileTree.File name="UriTooLong.ts" />
              <FileTree.File name="VariantAlsoNegotiates.ts" />
            </FileTree.Folder>
            <FileTree.File name="HTTPClient.ts" />
            <FileTree.File name="httpExceptions.ts" />
            <FileTree.File name="HTTPLibrary.ts" />
            <FileTree.File name="QuerySerializer.ts" />
          </FileTree.Folder>
          <FileTree.File name="index.ts" />
          <FileTree.File name="models.ts" />
          <FileTree.Folder name="services">
            <FileTree.Folder name="books">
              <FileTree.File name="Books.ts" />
              <FileTree.File name="index.ts" />
              <FileTree.Folder name="models">
                <FileTree.File name="AddBookRequest.ts" />
                <FileTree.File name="AddBookResponse.ts" />
                <FileTree.File name="GetAllBooksResponse.ts" />
                <FileTree.File name="GetBookByIdResponse.ts" />
                <FileTree.File name="UpdateBookCoverByIdRequest.ts" />
              </FileTree.Folder>
            </FileTree.Folder>
            <FileTree.Folder name="common">
              <FileTree.File name="Author.ts" />
              <FileTree.File name="FantasyBook.ts" />
              <FileTree.File name="ProgrammingBook.ts" />
              <FileTree.File name="SciFiBook.ts" />
            </FileTree.Folder>
            <FileTree.Folder name="orders">
              <FileTree.File name="index.ts" />
              <FileTree.Folder name="models">
                <FileTree.File name="CreateOrderRequest.ts" />
                <FileTree.File name="GetAllOrdersResponse.ts" />
                <FileTree.File name="OrderStreamMessage.ts" />
                <FileTree.File name="Order.ts" />
                <FileTree.File name="User.ts" />
              </FileTree.Folder>
              <FileTree.File name="Orders.ts" />
            </FileTree.Folder>
            <FileTree.File name="README.md" />
          </FileTree.Folder>
        </FileTree.Folder>
        <FileTree.Folder name="test">
          <FileTree.Folder name="models" />
          <FileTree.Folder name="services">
            <FileTree.Folder name="books">
              <FileTree.File name="Books.test.ts" />
            </FileTree.Folder>
            <FileTree.Folder name="orders">
              <FileTree.File name="Orders.test.ts" />
            </FileTree.Folder>
          </FileTree.Folder>
        </FileTree.Folder>
        <FileTree.File name="tsconfig.eslint.json" />
        <FileTree.File name="tsconfig.json" />
      </FileTree>
    </div>

  </div>
</div>

### SDK code comparison

With the bird's-eye view out of the way, let's take a closer look at the code.

#### 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](/post/type-safe-vs-type-faith).

The SDK created by Speakeasy uses [Zod](https://zod.dev/) 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](https://medium.com/distant-horizons/using-optimistic-ui-to-delight-your-users-ac819a81d59a) 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:

```yaml mark=18:21
!from ./openapi.yaml.txt 379:406
```

The highlighted `price` field above has type `integer`.

```typescript filename="speakeasy-example.ts" mark=17
// 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,
    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](https://zod.dev/?id=error-handling) 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 liblab 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 liblab may need to write additional client-side validation code to catch these errors before they are sent to the server.

#### Components compared

Speakeasy creates SDKs that contain rich type information for each component in your OpenAPI document. This includes clear definitions of enums, discriminated unions, and other complex types, augmented with runtime validation using Zod.

Let's compare the `Order` component from our OpenAPI document in the SDKs generated by Speakeasy and liblab.

Here's the `Order` component as generated by liblab:

```typescript filename="liblab/services/orders/models/Order.ts"
// This file was generated by liblab | https://liblab.com/

import { FantasyBook } from "../../common/FantasyBook";
import { ProgrammingBook } from "../../common/ProgrammingBook";
import { SciFiBook } from "../../common/SciFiBook";
import { User } from "./User";

type Status = "pending" | "shipped" | "delivered";

export interface Order {
  id: number;
  date: string;
  status: Status;
  user: User;
  products: (FantasyBook | ProgrammingBook | SciFiBook)[];
}
```

Note how the `products` field is defined as an array of `FantasyBook`, `ProgrammingBook`, or `SciFiBook`. This is a union type, but the SDK generated by liblab does not provide any runtime validation for this field, nor does it give any indication that this is a discriminated union.

Contrast this with the `Order` component as created by Speakeasy, which exports a useful `Products` type that is a discriminated union of `FantasyBook`, `ProgrammingBook`, and `SciFiBook`, along with a `Status` enum for use elsewhere in your code.

Verbose runtime validation is marked as `@internal` in the generated code, clearly indicating that it is not intended for direct use by developers, but rather for internal use by the SDK.

Here's the `Order` component created by Speakeasy:

```typescript filename="speakeasy/models/components/order.ts"
/*
 * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
 */

import * as z from "zod";
import { FantasyBook, FantasyBook$ } from "./fantasybook";
import { ProgrammingBook, ProgrammingBook$ } from "./programmingbook";
import { SciFiBook, SciFiBook$ } from "./scifibook";
import { User, User$ } from "./user";

export enum Status {
  Pending = "pending",
  Shipped = "shipped",
  Delivered = "delivered",
}

export type Products =
  | (ProgrammingBook & { category: "Programming" })
  | (FantasyBook & { category: "Fantasy" })
  | (SciFiBook & { category: "Sci-fi" });

export type Order = {
  id: number;
  date: Date;
  status: Status;
  user: User;
  products: Array<
    | (ProgrammingBook & { category: "Programming" })
    | (FantasyBook & { category: "Fantasy" })
    | (SciFiBook & { category: "Sci-fi" })
  >;
};

/** @internal */
export namespace Status$ {
  export const inboundSchema = z.nativeEnum(Status);
  export const outboundSchema = inboundSchema;
}

/** @internal */
export namespace Products$ {
  export const inboundSchema: z.ZodType<Products, z.ZodTypeDef, unknown> =
    z.union([
      ProgrammingBook$.inboundSchema.and(
        z
          .object({ category: z.literal("Programming") })
          .transform((v) => ({ category: v.category })),
      ),
      FantasyBook$.inboundSchema.and(
        z
          .object({ category: z.literal("Fantasy") })
          .transform((v) => ({ category: v.category })),
      ),
      SciFiBook$.inboundSchema.and(
        z
          .object({ category: z.literal("Sci-fi") })
          .transform((v) => ({ category: v.category })),
      ),
    ]);

  export type Outbound =
    | (ProgrammingBook$.Outbound & { category: "Programming" })
    | (FantasyBook$.Outbound & { category: "Fantasy" })
    | (SciFiBook$.Outbound & { category: "Sci-fi" });
  export const outboundSchema: z.ZodType<Outbound, z.ZodTypeDef, Products> =
    z.union([
      ProgrammingBook$.outboundSchema.and(
        z
          .object({ category: z.literal("Programming") })
          .transform((v) => ({ category: v.category })),
      ),
      FantasyBook$.outboundSchema.and(
        z
          .object({ category: z.literal("Fantasy") })
          .transform((v) => ({ category: v.category })),
      ),
      SciFiBook$.outboundSchema.and(
        z
          .object({ category: z.literal("Sci-fi") })
          .transform((v) => ({ category: v.category })),
      ),
    ]);
}

/** @internal */
export namespace Order$ {
  export const inboundSchema: z.ZodType<Order, z.ZodTypeDef, unknown> =
    z.object({
      id: z.number().int(),
      date: z
        .string()
        .datetime({ offset: true })
        .transform((v) => new Date(v)),
      status: Status$.inboundSchema,
      user: User$.inboundSchema,
      products: z.array(
        z.union([
          ProgrammingBook$.inboundSchema.and(
            z
              .object({ category: z.literal("Programming") })
              .transform((v) => ({ category: v.category })),
          ),
          FantasyBook$.inboundSchema.and(
            z
              .object({ category: z.literal("Fantasy") })
              .transform((v) => ({ category: v.category })),
          ),
          SciFiBook$.inboundSchema.and(
            z
              .object({ category: z.literal("Sci-fi") })
              .transform((v) => ({ category: v.category })),
          ),
        ]),
      ),
    });

  export type Outbound = {
    id: number;
    date: string;
    status: string;
    user: User$.Outbound;
    products: Array<
      | (ProgrammingBook$.Outbound & { category: "Programming" })
      | (FantasyBook$.Outbound & { category: "Fantasy" })
      | (SciFiBook$.Outbound & { category: "Sci-fi" })
    >;
  };

  export const outboundSchema: z.ZodType<Outbound, z.ZodTypeDef, Order> =
    z.object({
      id: z.number().int(),
      date: z.date().transform((v) => v.toISOString()),
      status: Status$.outboundSchema,
      user: User$.outboundSchema,
      products: z.array(
        z.union([
          ProgrammingBook$.outboundSchema.and(
            z
              .object({ category: z.literal("Programming") })
              .transform((v) => ({ category: v.category })),
          ),
          FantasyBook$.outboundSchema.and(
            z
              .object({ category: z.literal("Fantasy") })
              .transform((v) => ({ category: v.category })),
          ),
          SciFiBook$.outboundSchema.and(
            z
              .object({ category: z.literal("Sci-fi") })
              .transform((v) => ({ category: v.category })),
          ),
        ]),
      ),
    });
}
```

#### OAuth client credentials handling

Only Speakeasy's SDKs handle OAuth 2.0 with client credentials, including managing 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:

```yaml filename="openapi.yaml"
!from ./openapi.yaml.txt 633:639
```

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.

```typescript filename="speakeasy-example.ts"
// techbooks-speakeasy SDK created by Speakeasy
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();
```

The SDK generated by liblab does not support OAuth 2.0 client credentials at all.

#### Server-sent events (SSE) and streaming responses

Our bookstore API includes an operation that streams orders to the client using Server-Sent Events (SSE).

```yaml mark=15
paths:
  /orderstream:
    get:
      summary: Get a stream of orders
      operationId: getOrderStream
      description: Returns a stream of orders
      tags:
        - Orders
      security:
        - apiKey: []
      responses:
        "200":
          description: A stream of orders
          content:
            text/event-stream:
              schema:
                $ref: "#/components/schemas/OrderStreamMessage"
```

Let's see how the SDKs handle this.

[Speakeasy generates types and methods for handling SSE](/docs/customize-sdks/streaming) without any customization. Here's an example of how to use the SDK to listen for new orders:

```typescript filename="speakeasy-example.ts"
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](https://stoplight.io/) server.)

liblab does not generate SSE handling code. Developers using the SDK generated by liblab will need to write additional code to handle SSE.

#### Streaming uploads

Speakeasy supports streaming uploads without any custom configuration. OpenAPI operations with `multipart/form-data` content types are automatically handled as streaming uploads.

The following example illustrates how to use an SDK created by Speakeasy to upload a large file:

```typescript filename="speakeasy-example.ts" 
import { openAsBlob } from "node:fs";
import { SDK } from "@speakeasy/super-sdk";

async function run() {
  const sdk = new SDK();

  const fileHandle = await openAsBlob("./src/sample.txt");

  const result = await sdk.upload({ file: fileHandle });

  console.log(result);
}
run();
```

#### React Hooks

React Hooks simplify state and data management in React apps, enabling developers to consume APIs more efficiently.

liblab does not support React Hooks natively. Developers must manually integrate SDK methods into state management tools like React Context, Redux, or TanStack Query.

Speakeasy generates built-in React Hooks using [TanStack Query](https://tanstack.com/query/latest). These hooks provide features like intelligent caching, type safety, pagination, and seamless integration with modern React patterns such as SSR and Suspense.

Here's an example:

```typescript filename="example/loadPosts.tsx"
import { useQuery } from "@tanstack/react-query";

function Posts() {
  const { data, status, error } = useQuery([
    "posts" // Cache key for the query
  ], async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    return response.json();
  });

  if (status === "loading") return <p>Loading posts...</p>;
  if (status === "error") return <p>Error: {error?.message}</p>;

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.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.

#### Auto-pagination

Speakeasy's React Hooks also enable auto-pagination, which automatically fetches more data when the user scrolls to the bottom of the page. This feature is useful for infinite scrolling in social media feeds or search results.

```typescript filename="example/PostsView.tsx"
import { useInView } from "react-intersection-observer";

import { useActorAuthorFeedInfinite } from "@speakeasy-api/bluesky/react-query/actorAuthorFeed.js";

export function PostsView(props: { did: string }) {
  const { data, fetchNextPage, hasNextPage } = useActorAuthorFeedInfinite({
    actor: props.did,
  });

  const { ref } = useInView({
    rootMargin: "50px",
    onChange(inView) {
      if (inView) { fetchNextPage(); }
    },
  });

  return (
    <div>
      <ul className="space-y-4">
        {data?.pages.flatMap((page) => {
          return page.result.feed.map((entry) => (
            <li key={entry.post.cid}>
              <FeedEntry entry={entry.post} />
            </li>
          ));
        })}
      </ul>
      {hasNextPage ? <div ref={ref} /> : null}
    </div>
  );
```

liblab also supports pagination, however this requires additional configuration using the `liblab.config.json` file.

For an in-depth look at how Speakeasy generates React Hooks, see our [official release article](https://www.speakeasy.com/post/release-react-hooks).

### OpenAPI extensions and Overlays vs liblab config

Speakeasy embraces OpenAPI as the source of truth for generating SDKs. This means that Speakeasy does not require any additional configuration files to generate SDKs, apart from minimal configuration in the `gen.yaml` file.

Any configuration related to individual operations or components is done in the OpenAPI document itself, using OpenAPI extensions. Speakeasy provides a [list of supported OpenAPI extensions](/docs/customize-sdks) in its documentation.

If editing your OpenAPI document is not an option, Speakeasy also supports the [OpenAPI Overlays](/docs/prep-openapi/overlays/create-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.

Instead of using OpenAPI extensions, liblab uses a [configuration file](https://developers.liblab.com/cli/config-file-overview/) to customize SDKs. This configuration overrides many of the aspects Speakeasy allows you to configure in the OpenAPI document itself, or using overlays.

### Linting and change detection

Speakeasy's CLI includes a detailed and accurate linter that checks your OpenAPI document and provides feedback. This is especially useful during development, but can also catch errors in your CI/CD pipeline.

Speakeasy also keeps track of changes in your OpenAPI document, and versions the SDKs it creates based on those changes.

### Building for the browser

Both Speakeasy and liblab generate SDKs that can be used in the browser. To use the SDKs in the browser, you need to bundle your application using a tool like webpack, Rollup, or esbuild.

Speakeasy creates SDKs that are tree-shakeable, meaning that you can include only the parts of the SDK that you need in your application. This can help reduce the size of your application bundle.

Because Speakeasy SDKs include runtime type checking, the Zod library is included in the bundle. However, if you use Zod in other parts of your application, you can share the Zod instance between the SDK and your application to reduce the bundle size.

Here's an example of how to bundle an application that uses the Speakeasy SDK for the browser without Zod using esbuild:

```bash filename="Terminal" mark=7
npx esbuild src/speakeasy-app.ts \
  --bundle \
  --minify \
  --target=es2020 \
  --platform=browser \
  --outfile=dist/speakeasy-app.js \
  --external:zod
```

## 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](/post/compare-speakeasy-open-source).