TypeScript Design
OSS Comparison
For a comparison between the Speakeasy Go SDK and some popular open-source generators, see this page.
Speakeasy TypeScript SDKs are compatible with JavaScript projects out of the box and designed to be easy to use and easy to debug. Various decisions were made that guide the design of the SDK, including:
- Speakeasy uses the Axios Libary (opens in a new tab) with the fewest possible dependencies to make HTTP requests.
- Dynamic code generation based on the OpenAPI document.
- Speakeasy uses decorators for all generated models so that per-field metadata can be appended to correctly serialize and deserialize models based on the OpenAPI document.
- To take full advantage of the TypeScript type system, models and parameters are fully typed.
- To improve readability and avoid duplicating code, TypeScript SDKs include a
utils
module containing methods for configuring the SDK and serializing/deserializing generated types.
Dynamic Code Generation
Easy-to-use TypeScript SDKs that minimize the time to 200 should have minimal bloat and dead code in exported methods.
Speakeasy dynamically generates code for an SDK based on the OpenAPI document and will only include what is needed. For example, the Swagger Petstore OpenAPI schema (opens in a new tab) defines security at the operation-level for each endpoint. Swagger CodeGen (opens in a new tab) for TypeScript will create a global configuration object that contains fields like username
and password
for basic auth, even if this isn't a represented securityScheme
in the OpenAPI document, like so:
export class Configuration {
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string;
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string;
// ... <other security schemes>
}
Speakeasy-generated TypeScript SDKs will only generate the config fields that are actually used in the OpenAPI document. If the security is operation-specific, it will be defined as a field on the request at the appropriate scope. The following is an example for the getInventory
operation:
export class GetInventorySecurity extends SpeakeasyBase {
@SpeakeasyMetadata({data: "security, scheme=true;type=apiKey;subtype=header"})
apiKey: shared.SchemeApiKey;
}
export class GetInventoryRequest extends SpeakeasyBase {
@SpeakeasyMetadata()
security: GetInventorySecurity;
}
This makes it easier for you to inline the security config with a request, as shown below:
import {SDK, withSecurity} from "openapi";
import {GetInventoryRequest, GetInventoryResponse} from "openapi/src/sdk/models/operations";
import {AxiosError} from "axios";
const sdk = new SDK();
const req: GetInventoryRequest = {
security: {
apiKey: {
apiKey: "YOUR_API_KEY_HERE",
}
}
};
sdk.store.getInventory(req).then((res: GetInventoryResponse) => {
// handle response
});
This principle is not limited to security. A generated method, for example, will only contain references to a query parameter variable if it is specified under the operation in the OpenAPI document.
Fully Typed
For a better developer experience, Speakeasy TypeScript SDKs are fully typed, so an IDE can provide hints. Speakeasy generates types for components in your OpenAPI schema and for the requests and responses of each operation to make using your SDK more intuitive.
Consider a GET operation in your OpenAPI document that has a bunch of optional query parameters. The Swagger generator will generate an SDK containing a method that takes in all of these parameters in the method signature:
testQueryParams(firstParam ? : FirstParamType, secondParam ? : SecondParamType, thirdParam ? : ThirdParamType, fourthParam ? : FourthParamType)
{
// ...
}
Even though the parameters themselves are typed, this makes usage cumbersome, especially if a user only wants to set fourthParam
:
api.testQueryParams(undefined, undefined, undefined, {fourthParamFoo: "fourthParamBar"}).then((res) => {
// handle response
});
Using Speakeasy, the TypeScript SDK will generate a request object that contains all the optional parameters:
export class TestQueryParamsQueryParams extends SpeakeasyBase {
@SpeakeasyMetadata({data: "queryParam, style=form;explode=false;name=firstParam"})
firstParam?: FirstParamType;
@SpeakeasyMetadata({data: "queryParam, style=deepObject;explode=true;name=secondParam"})
secondParam?: SecondParamType;
@SpeakeasyMetadata({data: "queryParam, style=form;explode=true;name=thirdParam"})
thirdParam?: ThirdParamType;
@SpeakeasyMetadata({data: "queryParam, style=form;explode=true;name=fourthParam"})
fourthParam?: FourthParamType;
}
The method signature will only take in the request object:
testQueryParams(req
:
operations.TestQueryParamsRequest, config ? : AxiosRequestConfig
):
Promise < operations.TestQueryParamsResponse > {
// ...
}
This makes usage much easier:
sdk.testQueryParams({fourthParam: {fourthParamFoo: "fourthParamBar"}}).then((res) => {
// handle response
});
Decorators
Speakeasy generates models from the OpenAPI schema as classes, which allows for decorators to supply metadata for some fields.
Metadata is inspected at runtime to provide information for a particular field, which could be anything from serialization parameters to denoting what the field represents (for example, a security scheme, request parameter, or request body).
To avoid verbose SDK methods that reduce readability, Speakeasy defines helper methods in utils
that take in metadata to create requests, and SDK methods are callouts to these helper methods.
To illustrate, for an operation that requires oauth2
security and a request body, Swagger Codegen generates a method that looks like this:
createUserForm: async (id?: number, username?: string, firstName?: string, lastName?: string, email?: string, password?: string, phone?: string, userStatus?: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
...
// request body portion
if (id !== undefined) {
localVarFormParams.set('id', id as any);
}
if (username !== undefined) {
localVarFormParams.set('username', username as any);
}
if (firstName !== undefined) {
localVarFormParams.set('firstName', firstName as any);
}
if (lastName !== undefined) {
localVarFormParams.set('lastName', lastName as any);
}
if (email !== undefined) {
localVarFormParams.set('email', email as any);
}
if (password !== undefined) {
localVarFormParams.set('password', password as any);
}
if (phone !== undefined) {
localVarFormParams.set('phone', phone as any);
}
if (userStatus !== undefined) {
localVarFormParams.set('userStatus', userStatus as any);
}
localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded';
...
// security portion
// authentication petstore_auth required
// oauth required
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
? await configuration.accessToken("petstore_auth", ["write:pets", "read:pets"])
: await configuration.accessToken;
localVarHeaderParameter["Authorization"] = "Bearer " + localVarAccessTokenValue;
}
...
}
If the operation had query parameters, this method would be even more verbose. Furthermore, this method is only for a form-urlencoded
request body. If your OpenAPI document supports multiple media types for the body, Swagger Codegen will generate an entire method for each.
By contrast, the SDK methods generated by Speakeasy are more concise and DRY (opens in a new tab):
createUser(
req
:
operations.CreateUserRequest,
config ? : AxiosRequestConfig
):
Promise < operations.CreateUserResponse > {
...
// security portion
const client
:
AxiosInstance = utils.createSecurityClient(this._defaultClient!, req.security)!;
...
// request body portion
try {
[reqBodyHeaders, reqBody] = utils.serializeRequestBody(req);
} catch (e: unknown) {
if (e instanceof Error) {
throw new Error(`Error serializing request body, cause: ${e.message}`);
}
}
...
}
If you have any feedback or want to suggest improvements or ask for a new feature, please get in touch in the #client-sdks
channel in our public Slack (opens in a new tab) or drop us a note at info@speakeasyapi.dev.