Typescript Design
The Typescript SDKs are designed to be easy to use and easy to debug. Various decisions were made that guide the design of the SDK, these include:
- Using as little dependencies as possible, we use the Axios Libary to make our HTTP requests.
- Dynamic code generation based on the OpenAPI document.
- Using decorators for all the models we generate, this allows us to append per field metadata to correctly serialize and deserialize models based on the OpenAPI document.
- Fully typed models and parameters, taking full advantage of the typescript type system.
- Including a utils module that contains methods for configuring the SDK and serializing/deserializing the types we generate, to avoid duplication of code in each method reducing the readability.
Dynamic Code Generation
For Typescript SDKs to be easy to use and minimize the time to 200, they need to have minimal bloat/dead code in the methods exported for users.
This means that we need to generate code for the SDK dynamically based on the OpenAPI document to only
contain what's needed.For example, Swagger's Petstore OpenAPI schema has
security defined at the operation level for each endpoint.
Swagger CodeGen for Typescript will create a global configuration
object that contains fields,such as username/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>
}
On the other hand, Speakeasy-generated Typescript SDKs will only generate the config fields that are actually used in
the OpenAPI document.
If the security is operation-specific, then it'll 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;
}
As a user, 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
});
However, this principle is not just limited to security. A generated method for example will only contain references to a query parameter variable if specified under the operation it represents in the OpenAPI document.
Fully Typed
The Typescript SDKs are fully typed, allowing for a better developer experience as the IDE can provide hints. We generate types not only for components in your OpenAPI schema but also the requests and responses of each operation. This allows for more intuitive usage.
Consider a /GET operation in your OpenAPI document that has a bunch of optional query parameters. Swagger's 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 > {
// ...
}
Making usage much easier:
sdk.testQueryParams({fourthParam: {fourthParamFoo: "fourthParamBar"}}).then((res) => {
// handle response
});
Decorators
At this point, you may be wondering "what's with the @SpeakeasyMetadata
decorators?". We generate models from the
OpenAPI schema as classes, enabling
us to use decorators to supply metadata for some fields. This metadata is inspected at runtime to provide information
for a particular field.
This can be anything from serialization parameters to denoting whether the field represents a security scheme, request
parameter, request body, etc.
Helper methods are defined in utils
that take in this information and use it to create requests. This prevents the
need to
make SDK methods that are exposed to the user verbose, thereby improving readability. All the boilerplate of setting
security headers, serializing
operation parameters/request bodies, etc. is done once in utils
module, and the SDK methods are just call out to these
helper methods.
For example, for an operation that requires oauth2
security and a request body, swagger-codegen
will generate a
method
that look 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;
}
...
}
This would be even more verbose if the operation had query parameters, etc. 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 one.
On the contrary, the SDK methods generated by Speakeasy are more concise
and DRY:
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 contact in
the #client-sdks
channel in
our public Slack or drop us
a note at info@speakeasyapi.dev