# components.schemas.
  BookingPayment:
    type: object
    properties:
      amount:
        type: number
        description: Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected.
      currency:
        $ref: '#/components/schemas/Currency'
        description: Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase.
      source:
        oneOf:
          - title: Card
            type: object
            properties:
              object:
                type: string
                const: card
              name:
                type: string
              number:
                type: string
              cvc:
                type: string
                writeOnly: true
              exp_month:
                type: integer
                format: int64
              exp_year:
                type: integer
                format: int64
              address_post_code:
                type: string
            required:
              - name
              - number
              - cvc
              - exp_month
              - exp_year
          - title: Bank Account
            type: object
            properties:
              object:
                const: bank_account
                type: string
              name:
                type: string
              number:
                type: string
              sort_code:
                type: string
              bank_name:
                type: string
            required:
              - name
              - number
              - bank_name
```

How will each generator handle this polymorphic object? Let's find out! 

### Speakeasy type definitions

Speakeasy will generate union types for polymorphic objects, and use the discriminator to add runtime type casting for input and output objects. The following code is a snippet of types generated for the `BookingPayment` schema:

```ts
/**
 * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase.
 */
export const Currency = {
  Bam: "bam",
  Bgn: "bgn",
  Chf: "chf",
  Eur: "eur",
  Gbp: "gbp",
  Nok: "nok",
  Sek: "sek",
  Try: "try",
} as const;
/**
 * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase.
 */
export type Currency = ClosedEnum<typeof Currency>;

/**
 * A bank account to take payment from. Must be able to make payments in the currency specified in the payment.
 */
export type BankAccount = {
  object?: "bank_account" | undefined;
  name: string;
  /**
   * The account number for the bank account, in string form. Must be a current account.
   */
  number: string;
  /**
   * The sort code for the bank account, in string form. Must be a six-digit number.
   */
  sortCode?: string | undefined;
  /**
   * The name of the bank associated with the routing number.
   */
  bankName: string;
};

/**
 * A card (debit or credit) to take payment from.
 */
export type Card = {
  object?: "card" | undefined;
  /**
   * Cardholder's full name as it appears on the card.
   */
  name: string;
  /**
   * The card number, as a string without any separators. On read all but the last four digits will be masked for security.
   */
  number: string;
  /**
   * Card security code, 3 or 4 digits usually found on the back of the card.
   */
  cvc: string;
  /**
   * Two-digit number representing the card's expiration month.
   */
  expMonth: number;
  /**
   * Four-digit number representing the card's expiration year.
   */
  expYear: number;
  /**
   * Postal code associated with the card's billing address.
   */
  addressPostCode?: string | undefined;
};

/**
 * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking.
 */
export type Source = Card | BankAccount;

/**
 * A payment for a booking.
 */
export type BookingPayment = {
  /**
   * Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected.
   */
  amount?: number | undefined;
  /**
   * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase.
   */
  currency?: Currency | undefined;
  /**
   * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking.
   */
  source?: Card | BankAccount | undefined;
};
```

Reusing the descriptions as comments means the code is nicely decorated for anyone who goes prodding around, and by using docblock syntax it will be read by JS/TS documentation generators too.

The types are also defined and exported so they can be used in runtime code easily, instead of defined inline as many of the other generators do. This helps reuse throughout the rest of the SDK for request/responses, and allow for the most complex of scenarios to be handles easily. 

```ts
export type CreateBookingPaymentResponseBody$Outbound = {
  id?: string | undefined;
  amount?: number | undefined;
  currency?: string | undefined;
  source?: Card$Outbound | BankAccount$Outbound | undefined;
  status?: string | undefined;
  links?: models.LinksBooking$Outbound | undefined;
};
```

### Oazapfts type definition

Over to oazapfts, which sticks to its minimalist approach and generates one type for the request and one type for the response, with anything inside that being defined in line. If there are lots of shared parameters between requests and responses then these will be repeated, and that makes documentation and code suffer, but it keeps things simple. As for polymorphism, oazapfts handles union types with runtime type casting.

```ts
export type BookingPaymentRead = {
    /** Unique identifier for the payment. This will be a unique identifier for the payment, and is used to reference the payment in other objects. */
    id?: string;
    /** Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */
    amount?: number;
    /** Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */
    currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try";
    /** The status of the payment, one of `pending`, `succeeded`, or `failed`. */
    status?: "pending" | "succeeded" | "failed";
};
export type BookingPaymentWrite = {
    /** Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */
    amount?: number;
    /** Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */
    currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try";
    /** The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */
    source?: {
        "object"?: "card";
        /** Cardholder's full name as it appears on the card. */
        name: string;
        /** The card number, as a string without any separators. On read all but the last four digits will be masked for security. */
        "number": string;
        /** Card security code, 3 or 4 digits usually found on the back of the card. */
        cvc: string;
        /** Two-digit number representing the card's expiration month. */
        exp_month: number;
        /** Four-digit number representing the card's expiration year. */
        exp_year: number;
        /** The postal code associated with the card's billing address. */
        address_post_code?: string;
    } | {
        "object"?: "bank_account";
        name: string;
        /** The account number for the bank account, in string form. Must be a current account. */
        "number": string;
        /** The sort code for the bank account, in string form. Must be a six-digit number. */
        sort_code?: string;
        /** The name of the bank associated with the routing number. */
        bank_name: string;
    };
};
```

The verbosity of these types can be improved with the `--mergeReadWriteOnly` to combine the read and write models into one, but similar models with shared parameters will still be defining everything over again.

### OpenAPI Generated typescript-fetch type definitions

OpenAPI Generated's generated typescript-fetch SDK is much more verbose their either Speakeasy or Oazapfts. It does not seem too familiar with TypeScript and uses it rather loosely with a whole lot of if statements, and the bank account vs card payment logic really seems awkward.

```ts
/**
 * A card (debit or credit) to take payment from.
 * @export
 * @interface Card
 */
export interface Card {
    /**
     * 
     * @type {string}
     * @memberof Card
     */
    object?: CardObjectEnum;
    /**
     * Cardholder's full name as it appears on the card.
     * @type {string}
     * @memberof Card
     */
    name: string;
    /**
     * The card number, as a string without any separators. On read all but the last four digits will be masked for security.
     * @type {string}
     * @memberof Card
     */
    number: string;
    /**
     * Card security code, 3 or 4 digits usually found on the back of the card.
     * @type {string}
     * @memberof Card
     */
    cvc: string;
    /**
     * Two-digit number representing the card's expiration month.
     * @type {number}
     * @memberof Card
     */
    expMonth: number;
    /**
     * Four-digit number representing the card's expiration year.
     * @type {number}
     * @memberof Card
     */
    expYear: number;
    /**
     * 
     * @type {string}
     * @memberof Card
     */
    addressPostCode?: string;
}

/**
 * @export
 */
export const CardObjectEnum = {
    Card: 'card'
} as const;
export type CardObjectEnum = typeof CardObjectEnum[keyof typeof CardObjectEnum];


/**
 * Check if a given object implements the Card interface.
 */
export function instanceOfCard(value: object): value is Card {
    if (!('name' in value) || value['name'] === undefined) return false;
    if (!('number' in value) || value['number'] === undefined) return false;
    if (!('cvc' in value) || value['cvc'] === undefined) return false;
    if (!('expMonth' in value) || value['expMonth'] === undefined) return false;
    if (!('expYear' in value) || value['expYear'] === undefined) return false;
    return true;
}

export function CardFromJSON(json: any): Card {
    return CardFromJSONTyped(json, false);
}

export function CardFromJSONTyped(json: any, ignoreDiscriminator: boolean): Card {
    if (json == null) {
        return json;
    }
    return {
        'object': json['object'] == null ? undefined : json['object'],
        'name': json['name'],
        'number': json['number'],
        'cvc': json['cvc'],
        'expMonth': json['exp_month'],
        'expYear': json['exp_year'],
        'addressPostCode': json['address_post_code'] == null ? undefined : json['address_post_code'],
    };
}

export function CardToJSON(json: any): Card {
    return CardToJSONTyped(json, false);
}

export function CardToJSONTyped(value?: Card | null, ignoreDiscriminator: boolean = false): any {
    if (value == null) {
        return value;
    }

    return {
        'object': value['object'],
        'name': value['name'],
        'number': value['number'],
        'cvc': value['cvc'],
        'exp_month': value['expMonth'],
        'exp_year': value['expYear'],
        'address_post_code': value['addressPostCode'],
    };
}
```

It's even managed to output some syntax errors and import some dependencies that were not used. Was it meant to use those imports somewhere, or is it bringing in unnecessary dependencies? Unclear. 

![](/assets/docs/openapi-generator-ts-errors.png)

### OpenAPI Generator typescript-node type definitions

Finally, how about this OpenAPI Generator typescript-node template?

The typescript-node generator starts off looking simple enough, with a single type for any given payload:

```ts
/**
* A payment for a booking.
*/
export class BookingPayment {
    /**
    * Unique identifier for the payment. This will be a unique identifier for the payment, and is used to reference the payment in other objects.
    */
    'id'?: string;
    /**
    * Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected.
    */
    'amount'?: number;
    /**
    * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase.
    */
    'currency'?: BookingPayment.CurrencyEnum;
    'source'?: BookingPaymentSource;
    /**
    * The status of the payment, one of `pending`, `succeeded`, or `failed`.
    */
    'status'?: BookingPayment.StatusEnum;

    static discriminator: string | undefined = undefined;

    static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [
        {
            "name": "id",
            "baseName": "id",
            "type": "string"
        },
        {
            "name": "amount",
            "baseName": "amount",
            "type": "number"
        },
        {
            "name": "currency",
            "baseName": "currency",
            "type": "BookingPayment.CurrencyEnum"
        },
        {
            "name": "source",
            "baseName": "source",
            "type": "BookingPaymentSource"
        },
        {
            "name": "status",
            "baseName": "status",
            "type": "BookingPayment.StatusEnum"
        }    ];

    static getAttributeTypeMap() {
        return BookingPayment.attributeTypeMap;
    }
}

export namespace BookingPayment {
    export enum CurrencyEnum {
        Bam = <any> 'bam',
        Bgn = <any> 'bgn',
        Chf = <any> 'chf',
        Eur = <any> 'eur',
        Gbp = <any> 'gbp',
        Nok = <any> 'nok',
        Sek = <any> 'sek',
        Try = <any> 'try'
    }
    export enum StatusEnum {
        Pending = <any> 'pending',
        Succeeded = <any> 'succeeded',
        Failed = <any> 'failed'
    }
}
```

It's hoisted some of the properties up into enums, and namespaced them which is nice. The polymorphic `source` property is defined as a separate type `BookingPaymentSource` in its own file, and here is how that looks:

```ts
import { RequestFile } from './models';
import { BankAccount } from './bankAccount';
import { Card } from './card';

/**
* The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking.
*/
export class BookingPaymentSource {
    'object'?: BookingPaymentSource.ObjectEnum;
    'name': string;
    /**
    * The account number for the bank account, in string form. Must be a current account.
    */
    'number': string;
    /**
    * Card security code, 3 or 4 digits usually found on the back of the card.
    */
    'cvc': string;
    /**
    * Two-digit number representing the card\'s expiration month.
    */
    'expMonth': number;
    /**
    * Four-digit number representing the card\'s expiration year.
    */
    'expYear': number;
    'addressPostCode'?: string;
    /**
    * The sort code for the bank account, in string form. Must be a six-digit number.
    */
    'sortCode'?: string;
    /**
    * The name of the bank associated with the routing number.
    */
    'bankName': string;

    static discriminator: string | undefined = undefined;

    static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [
        {
            "name": "object",
            "baseName": "object",
            "type": "BookingPaymentSource.ObjectEnum"
        },
        {
            "name": "name",
            "baseName": "name",
            "type": "string"
        },
        {
            "name": "number",
            "baseName": "number",
            "type": "string"
        },
        {
            "name": "cvc",
            "baseName": "cvc",
            "type": "string"
        },
        {
            "name": "expMonth",
            "baseName": "exp_month",
            "type": "number"
        },
        {
            "name": "expYear",
            "baseName": "exp_year",
            "type": "number"
        },
        {
            "name": "addressPostCode",
            "baseName": "address_post_code",
            "type": "string"
        },
        {
            "name": "sortCode",
            "baseName": "sort_code",
            "type": "string"
        },
        {
            "name": "bankName",
            "baseName": "bank_name",
            "type": "string"
        }    ];

    static getAttributeTypeMap() {
        return BookingPaymentSource.attributeTypeMap;
    }
}

export namespace BookingPaymentSource {
    export enum ObjectEnum {
        BankAccount = <any> 'bank_account'
    }
}
```

This is completely incorrect, as the `oneOf` for `Card` and `BankAccount` has been flattened into a single class with all the properties of both types. This means that when creating a `BookingPaymentSource` object, all properties from both `Card` and `BankAccount` are available, which is not the intended behavior at all.

Some older generators require the optional `discriminator` property in the OpenAPI document to handle scenarios that a `oneOf` should otherwise handle by itself, but even adding that doesn't help here.

```yaml
source:
  oneOf:
    - $ref: '#/components/schemas/Card'
    - $ref: '#/components/schemas/BankAccount'
  discriminator:
    propertyName: object
```

It still produces the exact same output.

### Type generation summary

Here's a summary of how each generator handles OpenAPI polymorphism:

<Table
  data={[
    {
      name: "Adds union types",
      speakeasy: "✅",
      "og-fetch": "✅",
      "og-node": "❌",
      oazapfts: "✅",
    },
    {
      name: "Supports discriminator",
      speakeasy: "✅",
      "og-fetch": "✅",
      "og-node": "❌",
      oazapfts: "✅",
    },
  ]}
  columns={[
    { key: "name", header: "" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "og-fetch", header: "OG Fetch" },
    { key: "og-node", header: "OG Node" },
    { key: "oazapfts", header: "Oazapfts" },
  ]}
/>

## Retries

The SDK managed by Speakeasy can automatically retry failed network requests or retry requests based on specific error responses, providing a straightforward developer experience for an otherwise complicated topic.

To enable this feature use the Speakeasy `x-speakeasy-retries` extension in the OpenAPI document. Here is an example updating `openapi.yaml` to add retries to the `create-booking` operation.

```yaml
x-speakeasy-retries:
  strategy: backoff
  backoff:
    initialInterval: 500 # 500 milliseconds
    maxInterval: 60000 # 60 seconds
    maxElapsedTime: 3600000 # 5 minutes
    exponent: 1.5
```

Add this snippet to the operation:

```yaml
#...
paths:
  /bookings:
    # ...
    post:
      #...
      operationId: create-booking
      x-speakeasy-retries:
        strategy: backoff
        backoff:
          initialInterval: 500 # 500 milliseconds
          maxInterval: 60000 # 60 seconds
          maxElapsedTime: 3600000 # 5 minutes
          exponent: 1.5
```

Now we'll rerun the Speakeasy generator to enable retries, and the SDK will automatically attempt to retry failed network requests when booking a trip. 

It is also possible to enable retries for the SDK as a whole by adding a global `x-speakeasy-retries` at the root of the OpenAPI document instead of per operation.

## React Hooks

[React Hooks](https://react.dev/reference/react/hooks) are a popular way to manage state and side effects in React applications.

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.

```ts filename="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.

None of the other generators generate React Hooks for their SDKs.

<Table
  data={[
    {
      name: "React Hooks",
      speakeasy: "✅",
      node: "❌",
      fetch: "❌",
      oazapfts: "❌",
      codegen: "❌",
    },
  ]}
  columns={[
    { key: "name", header: "" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "node", header: "Node" },
    { key: "fetch", header: "Fetch" },
    { key: "oazapfts", header: "Oazapfts" },
  ]}
/>

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

## Pagination

SDKs managed by Speakeasy include optional [pagination for OpenAPI operations](/docs/customize/runtime/pagination).

We'll update our pet store schema to add an `x-speakeasy-pagination` extension and a `page` query parameter:

```yaml
paths:
  /stations:
    get:
      x-speakeasy-pagination:
        type: offsetLimit
        inputs:
          - name: page 
            in: parameters 
            type: page
        outputs:
          results: $
      parameters:
        - name: page
          in: query
          description: The offset to start from
          required: false
          schema:
            type: integer
            default: 0
```

After regenerating the SDK with Speakeasy, the `get-stations` operation is automatically paginated, and can be iterated through with async/await until the clients needs are met.

```ts
import { TrainTravel } from "train-travel-sdk";

const trainTravel = new TrainTravel({
  oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "",
});

async function run() {
  const result = await trainTravel.stations.get({
    coordinates: "52.5200,13.4050",
    search: "Milano Centrale",
    country: "DE",
  });

  for await (const page of result) {
    console.log(page);
  }
}

run();
```

None of the other generators include pagination as a feature, leaving it all to the API client developers to figure out.

<Table
  data={[
    {
      name: "Adds pagination",
      speakeasy: "✅",
      node: "❌",
      fetch: "❌",
      oazapfts: "❌",
    },
  ]}
  columns={[
    { key: "name", header: "" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "og-node", header: "OG Node" },
    { key: "og-fetch", header: "OG Fetch" },
    { key: "oazapfts", header: "Oazapfts" },
  ]}
/>

## Streaming files & data

All the generators in our comparison generate SDKs that use the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), which enables streaming for large uploads or downloads. Speakeasy makes this clear by providing documentation showing how to use streaming for file uploads in the README. It's important to show developer-users how to take advantage of this streaming, and helping them handle large file uploads in different runtimes will cut down on support interactions.

```typescript filename="example/train-travel-sdk/README.md" mark=7:13
import { TrainTravel } from "train-travel-sdk";

const trainTravel = new TrainTravel({
  oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "",
});

async function run() {
  const result = await trainTravel.bookings.createRaw(
    bytesToStream(
      new TextEncoder().encode(
        "{\"trip_id\":\"4f4e4e1-c824-4d63-b37a-d8d698862f1d\",\"passenger_name\":\"John Doe\"}",
      ),
    ),
  );

  console.log(result);
}

run();
```

Beyond simply uploading and download files with streaming, Speakeasy SDKs also support JSON streaming for large JSON payloads using standards and conventions like [JSONL](https://jsonlines.org/) or [ND-JSON](https://ndjson.org/). This is particularly useful when dealing with large datasets that may not fit into memory all at once. Speakeasy provides built-in support for JSON streaming, allowing developers to process JSON data in chunks as it is received.

```typescript
import { SDK } from '@speakeasy/sdk';

const sdk = new SDK();

async function streamLogs() {
  const result = await sdk.logs.fetch_logs();

  for await (const event of result) {
    // Each event is a parsed JSON object from the stream
    console.log(`[${event.timestamp}] ${event.message}`);
  }
}

streamLogs().catch(error => {
  console.error('Error streaming logs:', error);
});
```

OpenAPI Generator and Oazapfts do not support JSON streaming in their generated SDKs, which limits their ability to handle large JSON payloads efficiently. 

There's an extra issue with OpenAPI Generator's typescript-node SDK, in that its content negotiation strategy (looking at `Accept` and `Content-Type` headers) is overly simplistic. It checks if the content type includes `application/json` with the line `if (produces.indexOf('application/json') >= 0) {` which is too broad, because `application/jsonl` will match that condition. If an API returns `application/jsonl` it will try to parse the response as a single JSON object instead of a stream of JSON objects, which will lead to runtime errors and frustrated developer-users.

<Table
  data={[
    {
      name: "Stream uploads",
      speakeasy: "✅",
      "og-node": "✅",
      "og-fetch": "✅",
      oazapfts: "✅",
    },
    {
      name: "JSON streaming",
      speakeasy: "✅",
      "og-node": "❌",
      "og-fetch": "❌",
      oazapfts: "❌",
    },
    {
      name: "Documentation for streaming",
      speakeasy: "✅",
      "og-node": "❌",
      "og-fetch": "❌",
      oazapfts: "❌",
    },
  ]}
  columns={[
    { key: "name", header: "" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "og-node", header: "OG Node" },
    { key: "og-fetch", header: "OG Fetch" },
    { key: "oazapfts", header: "Oazapfts" },
  ]}
/>


## Generated documentation

Of all the generators tested, Speakeasy was the only one to generate documentation and usage examples for SDKs. Speakeasy considers documentation generation as a crucial feature to enable rapid adoption and ease of use when an SDK can be published to NPM, and not something that should be left to the API team to produce from scratch.

<Table
  data={[
    {
      name: "Adds documentation",
      speakeasy: "✅",
      "og-node": "❌",
      "og-fetch": "❌",
      oazapfts: "❌",
    },
    {
      name: "Adds usage examples",
      speakeasy: "✅",
      "og-node": "❌",
      "og-fetch": "❌",
      oazapfts: "❌",
    },
  ]}
  columns={[
    { key: "name", header: "" },
    { key: "speakeasy", header: "Speakeasy" },
    { key: "og-node", header: "OG Node" },
    { key: "og-fetch", header: "OG Fetch" },
    { key: "oazapfts", header: "Oazapfts" },
  ]}
/>

Speakeasy generates a `README.md` generated at the root of the SDK, [which you can customize](/docs/sdk-docs/edit-readme) to add branding, support links, a code of conduct, and any other information your developer-users might find helpful.

The Speakeasy SDK also includes working usage examples for all operations, complete with imports and appropriately formatted examples from the OpenAPI description. This is a huge help to developers getting started with the SDK, as they can copy and paste working code snippets directly into their applications. Here's an example of an operation from the Train Travel SDK's `README.md`:

```ts filename="example/train-travel-sdk/README.md" mark=8:12
import { TrainTravel } from "train-travel-sdk";

const trainTravel = new TrainTravel({
  oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "",
});

async function run() {
  const result = await trainTravel.stations.get({
    coordinates: "52.5200,13.4050",
    search: "Milano Centrale",
    country: "DE",
  });

  for await (const page of result) {
    console.log(page);
  }
}

run();
```

## Bundling applications for the browser

Speakeasy creates SDKs that are [tree-shakable](https://webpack.js.org/guides/tree-shaking/) and can be bundled for the browser using tools like Webpack, Rollup, or esbuild.

Because Speakeasy supports a wider range of OpenAPI features, Speakeasy-created SDKs are likely to be slightly larger than those generated by other tools. Speakeasy also limits abstraction, which can lead to larger SDKs. This does not translate to a larger bundle size, as the SDK can be tree-shaken to remove unused code.

Any SDK that supports runtime type checking or validation will have a larger bundle size, but the benefits of type checking and validation far outweigh the cost of a slightly larger bundle. If you use the popular validation library [Zod](https://zod.dev/) in your application already, you can exclude it from the SDK bundle to reduce its size.

Here's an example of how to exclude Zod from the SDK bundle:

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

## A live example: Vessel API Node SDK

[Vessel](https://www.vessel.dev/) trusts Speakeasy to generate and publish SDKs for its widely used APIs. We recently spoke to Zach Kirby about how Vessel uses Speakeasy. Zach shared that [the Vessel Node SDK](https://www.npmjs.com/package/@vesselapi/nodesdk) is downloaded from npm hundreds of times a week.

## Summary

The open-source SDK generators we tested are all good and clearly took tremendous effort and community coordination to build and maintain. Different applications have widely differing needs, and smaller projects may not need all the features offered by Speakeasy.

If you are building an API that developers rely on and would like to publish full-featured SDKs that follow best practices, we strongly recommend giving the [Speakeasy SDK generator](/docs/speakeasy-reference/cli/getting-started) a try.

[Join our Slack community](https://go.speakeasy.com/slack) to let us know how we can improve our TypeScript SDK generator or to suggest features.