Create TypeScript SDKs from OpenAPI / Swagger
SDK Overview
Speakeasy's TypeScript SDK creation is designed to build idiomatic TypeScript libraries, using standard web platform features.
The SDK is strongly typed, makes minimal use of third party modules, and is straight-forward to to debug. It should feel familiar to TypeScript developers when they use SDKs we generate. We make opinionated choices in some places, but we do so in a thoughtful and deliberate way.
The core features of the TypeScript SDK include:
- Compatibility with vanilla JavaScript projects since the SDK's consumption is through .d.ts (TypeScript type definitions) and .js files.
- Usable on the server and in the browser
- Use of the
fetch
,ReadableStream
and async iterable APIs for compatibility with all the popular JavaScript runtimes:- Node.js
- Deno
- Bun
- Support for streaming requests and responses
- Authentication support for OAuth flows as well as support for standard security mechanisms (HTTP Basic, application tokens, etc.)
- Optional pagination support for supported APIs
- Optional support for retries in every operation
- Complex number types including big integers and decimal
- Date and date/time types using RFC3339 date formats
- Custom type enums using strings and ints
- Union types and combined types
TypeScript Package Structure
Runtime Environment Requirements
The SDK targets ES2018, ensuring compatibility with a wide range of JavaScript runtimes that support this version. Key features required by the SDK include:
- Web Fetch API (opens in a new tab)
- Web Streams API (opens in a new tab) and in particular
ReadableStream
- Async iterables (opens in a new tab) using
Symbol.asyncIterator
Runtime environments that are explicitly supported are:
- Evergreen browsers: Chrome, Safari, Edge, Firefox
- Node.js active and maintenance LTS releases
- Currently, this is v18 and v20
- Bun v1 and above
- Deno v1.39 - Note that Deno does not currently have native support for streaming file uploads backed by the filesystem (issue link (opens in a new tab))
Note
For teams interested in working directly with the SDK's source files our SDK leverages TypeScript v5
features. This means that to directly consume these source files, your environment should support TypeScript version 5 or higher. However, this requirement is specific only to scenarios where direct access to the source is necessary.
TypeScript HTTP Client
TypeScript SDKs stick as close to modern and ubiquitous web standards as possible. We use the fetch()
API as our HTTP client. This API includes all the necessary building blocks to make HTTP requests: fetch
, Request
, Response
, Headers
, FormData
, File
and Blob
.
The standard nature of this SDK means it will work in modern JavaScript runtimes. This includes: Node.js, Deno, Bun, React Native. Weβve already been able to run our extensive suite to confirm that new SDKs work in Node.js, Bun and browsers.
Type System
Primitive Types
Where possible the TypeScript SDK preferences primitive types such as string
, number
and boolean
. However, in the case of arbitrary-precision decimals, a third-party library is used since there isn't a native decimal type. Using decimal types is crucial in certain applications such as code manipulating monetary amounts and in situations where overflow, underflow, or truncation caused by precision loss can lead to significant incidents.
To describe a decimal type in OpenAPI, you can use the format: decimal
keyword. The SDK will take care of serializing and deserializing decimal values under the hood using the decimal.js (opens in a new tab) library.
import { SDK } from "@speakeasy/super-sdk";import { Decimal } from "@speakeasy/super-sdk/types";const sdk = new SDK();const result = await sdk.payments.create({ amount: new Decimal(0.1).add(new Decimal(0.2))});
Similar to decimal types, there are numbers too large to be represented using JavaScriptβs Number type. For this reason, weβve introduced support for native BigInt
values.
In an OpenAPI schema, fields that are big integers can be modelled as strings with format: bigint
.
import { SDK } from "@speakeasy/super-sdk";const sdk = new SDK();const result = await sdk.doTheThing({ value: 67_818_454n, value: BigInt("340656901")});
Generated Types
The TypeScript SDK generates a type for each request and response models as well as for each shared model in your OpenAPI schema. Each model is backed by a Zod (opens in a new tab) schema which is used to validate the objects at runtime.
Note
It's important to that data validation is run on both user input when calling an SDK method AND on the subsequent response data from the server. If servers are not returning data that matches the OpenAPI spec then validation errors are thrown at runtime.
Below is a complete example of a shared model created by the TypeScript generator:
Public type
The type DrinkOrder
represents the public type that the model file exports. This is what users of the SDK will work with in their code.
Internal types
A special namespace accompanies every model and contains the types and schemas for the model that represent inbound and outbound data.
This namespace, including types and values within it, is not intended for use outside the SDK and as such it is marked as
@internal
.
The inbound representation of a model defines the shape of the data that is received from a server. It is validated and deserialized into the public type above.
The outbound representation of a model defines the shape of the data that is sent to a server. A user provides a value that satisfies the public type above and the outbound schema serializes it into what the server expects.
All generated models have this overall structure. By pinning the types with runtime validation using, we give users stronger guarantees that the SDK types they work with at development time continue to be valid at runtime otherwise we throw exceptions i.e. fail loudly.
Public type
The type DrinkOrder
represents the public type that the model file exports. This is what users of the SDK will work with in their code.
Internal types
A special namespace accompanies every model and contains the types and schemas for the model that represent inbound and outbound data.
This namespace, including types and values within it, is not intended for use outside the SDK and as such it is marked as
@internal
.
The inbound representation of a model defines the shape of the data that is received from a server. It is validated and deserialized into the public type above.
The outbound representation of a model defines the shape of the data that is sent to a server. A user provides a value that satisfies the public type above and the outbound schema serializes it into what the server expects.
All generated models have this overall structure. By pinning the types with runtime validation using, we give users stronger guarantees that the SDK types they work with at development time continue to be valid at runtime otherwise we throw exceptions i.e. fail loudly.
Union Types
Support for polymorphic types is critical to most production applications. In OpenAPI, these types are defined using the oneOf keyword. We represent these using TypeScript's union notation e.g. Cat | Dog
.
import { SDK } from "@speakeasy/super-sdk";async function run() { const sdk = new SDK(); const pet = await sdk.fetchMyPet(); switch (pet.type) { case "cat": console.log(pet.litterType); break; case "dog": console.log(pet.favoriteToy); break; default: // Ensures exhaustive switch statements in TypeScript pet satisfies never; throw new Error(`Unidentified pet type: ${pet.type}`) }}run();
Type Safety
TypeScript provides static type safety to give you greater confidence in the code your shipping. However, TypeScript has limited support to protect from opaque data at the boundaries of your programs. User input and server data coming across the network can circumvent static typing if not correctly modelled. This usually means marking this data as unknown
and exhaustively sanitizing it.
Our TypeScript SDKs solve this issue neatly by modelling all the data at the boundaries using Zod schemas (opens in a new tab). That ensures that everything coming from users and servers will work as intended, or fail loudly with clear validation errors. This is even more impactful for the vanilla JavaScript developers using your SDK.
import { SDK } from "@speakeasy/super-sdk";async function run() { const sdk = new SDK(); const result = await sdk.products.create({ name: "Fancy pants", price: "ummm" });}run();// π¨ Throws//// ZodError: [// {// "code": "invalid_type",// "expected": "number",// "received": "string",// "path": [// "price"// ],// "message": "Expected number, received string"// }// ]
While validating user input is considered table stakes for SDKs, itβs especially useful to validate server data given the information we have in your OpenAPI spec. This can help detect drift between schema and server and prevent certain runtime issues such as missing response fields or sending incorrect data types.
Tree Shaking
Speakeasy created Typescript SDKs contain few internal couplings between modules. This means users that are bundling them into client-side apps can take advantage of tree-shaking performance when working with "deep" SDKs. These are SDKs that are subdivided into namespaces such as sdk.comments.create(...) and sdk.posts.get(...). Importing the top-level SDK will pull in the entire SDK into a client-side bundle even if a small subset of functionality was needed.
It's now possible to import the exact namespaces, or "sub-SDKs", and tree-shake the rest of the SDK away at build time.
import { PaymentsSDK } from "@speakeasy/super-sdk/sdk/payments";// π Only code needed by this SDK is pulled in by bundlersasync function run() { const payments = new PaymentsSDK({ authKey: "" }); const result = await payments.list(); console.log(result);}run();
We benchmarked whether there would be benefits in allowing users to import individual SDK operations but from our testing it seems that this only yielded marginal reduction in bundled code versus importing sub-SDKs. It's highly dependent on how operations are grouped and the depth and breadth of an SDK as defined in the OpenAPI spec. If you think your SDK users could greatly benefit from exporting individual operations then please reach out to us and we can re-evaluate this feature.
Streaming Support
Support for streaming is critical for applications that need to send or receive large amounts of data between client and server without first buffering the data into memory, potentially exhausting this system resource. Uploading a very large file is one use case where streaming can be useful.
As an example, in Node.js v20, streaming a large file to a server using an SDK is only a handful of lines:
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();
On the browser, users would typically select files using <input type="file">
and the SDK call is identical to the sample code above.
Other JavaScript runtimes may have similar native APIs to obtain a web-standards File or Blob and pass it to SDKs.
For response streaming, SDKs expose a ReadableStream, a part of the Streams API web standard.
import fs from "node:fs";import { Writable } from "node:stream";import { SDK } from "@speakeasy/super-sdk";async function run() { const sdk = new SDK(); const result = await sdk.usageReports.download("UR123"); const destination = Writable.toWeb( fs.createWriteStream("./report.csv") ); await result.data.pipeTo(destination);}run();
Server-sent events
TypeScript SDKs support streaming of server-sent events by exposing an async iterables. Unlike the native EventSource
API, SDKs can create streams using GET or POST requests, as well as other methods, that can pass custom headers and request bodies.
import { SDK } from "@speakeasy/super-sdk";async function run() { const sdk = new SDK(); const result = await sdk.completions.chat({ messages: [ { role: 'user', content: "What is the fastest bird that is common in North America?", }, ], }); if (result.chatStream == null) { throw new Error("failed to create stream: received null value"); } for await (const event of res.chatStream) { process.stdout.write(event.data.content) }}run();
For more information on how to model this API in your OpenAPI document, check this page.
Parameters
If configured we will generate methods with parameters for each of the parameters defined in the OpenAPI document, as long as the number of parameters is less than or equal to the configured maxMethodParams
value in the gen.yaml
file.
If the number of parameters exceeds the configured maxMethodParams
value or this is set to 0
then a request object is generated for the method instead that allows for all parameters to be passed in a single object.
Errors
Following TypeScript best practices, all operation methods in the SDK will return a response object and an error. Callers should always check for the presence of the error. The object used for errors is configurable per request. Any error response may return a custom error object. A generic error will be provided when any sort of communication failure is detected during an operation.
Here's an example of custom error handling in a theoretical SDK:
import { Speakeasy } from "@speakeasy/bar";import * as errors from "@speakeasy/bar/sdk/models/errors";async function run() { const sdk = new Speakeasy({ apiKey: "<YOUR_API_KEY_HERE>", }); const res = await sdk.bar.getDrink().catch((err) => { if (err instanceof errors.FailResponse) { console.error(err); // handle exception return null; } else { throw err; } }); if (res?.statusCode !== 200) { throw new Error("Unexpected status code: " + res?.statusCode || "-"); } // handle response}run();
The SDK also includes a SDKValidationError
that will make it easier to debug validation errors particularly when the server sends unexpected data. Instead of throwing a ZodError back at SDK users and that didn't let you see what the
underlying raw data was that failed validation. SDKValidationError
solves this problem and provides a way to pretty-print the validation errors for a more pleasant debugging experience.
Debugging support
The typescript SDKs support a new response format that will include the native Request / Response objects that were used in an SDK method call. This can be enabled by setting the responseFormat
config in your gen.yaml
file to envelope-http
.
const sdk = new SDK();const { users, httpMeta } = await sdk.users.list();// πconst { request, response } = httpMeta;console.group("Request completed")console.log("Endpoint:", request.method, request.url)console.log("Status", response.status)console.log("Content type", response.headers.get("content-type"))console.groupEnd()
The httpMeta
property will also be available on any error class that relates to HTTP requests. This includes the built-in SDKError
class and any custom error classes that you have defined in your spec.
User Agent Strings
The Typescript SDK will include a user agent (opens in a new tab) string in all requests. This can be leveraged for tracking SDK usage amongst broader API usage. The format is as follows:
speakeasy-sdk/typescript {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}}
Where
SDKVersion
is the version of the SDK, defined ingen.yaml
and releasedGenVersion
is the version of the Speakeasy generatorDocVersion
is the version of the OpenAPI documentPackageName
is the name of the package defined ingen.yaml