/tsoapy

TypeScript OpenAPI Client

Primary LanguageTypeScriptMIT LicenseMIT

🧼 TSoapy
Type-Safe OpenAPI Calls

Create a fully-typed & lightweight client for your openapi-typescript type definitions. Zero dependencies. 2.4k. Optimized for OpenAPI v3.

Usage

Taken from the Pet Store Example

import type { paths } from "../__generated__/petstore";
import { tsoapy } from "../";

const client = tsoapy<paths>(new URL("https://example.com/api/petstore"));

// call your api using .path().method() to select the specific endpoint you need
const r1 = await client
  .path("/store/order")
  .method("post")
  // then, include your body, set params, query string, etc
  .body({
    id: 123,
    petId: 456,
    quantity: 1,
    shipDate: "2023-02-25T00:00:00Z",
    status: "placed",
    complete: false,
  })
  // send the request and get a descriminated union as a response
  .send();

if (r1.status === 200) {
  // union is discriminated on .status, and contains a .data property
  r1.data.id; // <= typed from response
}

API

Descriptions here provide a generic type such as string for clarity, but use a more complex typing in the codebase

tsoapy<paths>(url: URL, ctx?: ExtendedRequestInit)

Configure the initial value of tsoapy, including the URL base all path values are derrived from, and any RequestInit options you want to share between all chained methods.

Returns: An object containing the single .path() method.

arg description
url URL The endpoint URL for your OpenAPI calls
ctx Options to persist into fetch calls. You may also specify ctx.fetch to use a non-global fetch instance

tsoapy().path(path: string)

Select a path from the tsoapy collection.

Returns: An object containing the single .method() method.

arg description
path string A path contained in your OpenAPI types

tsoapy().path().method(method: string, contentType?: string, serialize: Function)

Select a method from the tsoapy collection, constrained to the previously built path. Because OpenAPI JSON is top-down desiged (path => method => params), you need to have a path before you can select a method. During method selection, you can specify a content type other than the default application/json, and set up a custom serialize function for converting your request object to a string.

Returns: A tsoapy builder object for configuring the request data, containing params, query, body, and send

arg description
method string A method contained in OpenAPI[path]
contentType string A supported content type found in OpenAPI[path][method].requestBody.content
serialize function A function that takes in the arguments of OpenAPI[path][method].requestBody.content[contentType] and returns a string. Defaults to JSON.serialize for application/json and toString() for other content types.

Tsoapy Builder Builder

After chaining tsoapy().path().method(), the API opens up in order to configure and send the request. Depending on your OpenAPI endpoint, you may need to set URL parameters, configure query string values, or set the request body. Once configured, you can send the request via .send(), returning a promise containing the API result.

tsoapy().path().method()....params(params: object)

Set URL parameters in a request such as /pet/{petId}.

Returns: Builder object for configuring the request data, containing params, query, body, and send

arg description
params object Details parameters to substitue into the url, such as /pet/{petId}

tsoapy().path().method()....query(query: object)

Set query string parameters for the request.

Returns: Builder object for configuring the request data, containing params, query, body, and send

arg description
query object Details query string values to append onto the url

tsoapy().path().method()....body(body: object)

Set the body for the request. Will use serializer to conver the object to a suitable string, defaulting to JSON.stringify().

Returns: Builder object for configuring the request data, containing params, query, body, and send

arg description
body object Operation variables. Will be serialized to a string using the provided serialize, and falling back to JSON.stringify for the application/json content type and toString() for all others

tsoapy().path().method()....send(options?: RequestInit, contentType?: string, deserialize?: Function):Promise

Send the request via fetch.

arg description
options RequestInit Additional fetch options, in addition to any specified in the original tsoapy call
contentType string A supported content type found in OpenAPI[path][method].responses[*].content
deserialize function A function that takes in a string and statusCode, and returns a value consistent with OpenAPI[path][method].responses[statusCode].content[contentType]. Defaults to JSON.parse for application/json and toString() for other content types.

Returns: A tsoapy response as a discriminated union in the following format:

{
  status: number;
  data: object;
}
property description
status number Discriminator based on the response code from the server
data object Content from the request

Sending Content Types other than application/json

Tsoapy uses application/json by default for requests & responses. If you need to change this, you can control the input and output handing.

  • input typing and serialization is controlled when calling .method(). In addition to setting the content type (which selects the responseBody from OpenAPI), the third parameter serialize allows you to pass in a custom serializer function for turning the JSON object into the requested content type, expressed as a string. Because there's a variety of content types, it's beyond Tsoapy to add custom converters for every possible serialization.
  • output typing and deserialization is controlled when calling .send(). Like method, you can pass a content type as the second argument which maps to a content type inside of the OpenAPI responses.[code] collection. The third parameter deserialize can be a function that takes in a string and returns a JSON object conforming to the object schema on OpenAPIs side. Like serialization, adding custom deserialization goes beyond the scope of this library.

⚠️ note You'll need to specify your content type and serializer/deserializer every time, as OpenAPI is designed in a top-down method of path => method => requestBody => content => [contentType] and path => method => responses => [code] => content => [contentType], making it difficult to set these at the top-level tsoapy() as you do not know at that time which path & method (and therefore which content types) you are invoking.

Common Serializer / Deserializers

If JSON isn't sufficient and you do need to serialize / deserialize in order to talk to your OpenAPI endpoint, there are already excellent battle-tested libraries that can be dropped into tsoapy without much effort.

FAQ

  • Why is it so small? Tsoapy is 90% type management. The exported index.js is the builder API, designed to build the fetch call in a way consistent with how you walk the OpenAPI paths object. In 0.0.4, the ESM version is 2.4kb and the CJS version is 3.43kb with the increase being the CommonJS harness provided by tsup.
  • Why not openapi? openapi is an excellent library, but its types are not as semantically correct as those generated by openapi-typescript. By separating type generation from runtime library, you can stop using tsoapy, go back to plain fetch and still take advantage of the types generated by openapi-typescript. This also means that tsoapy can focus on being a really good fetch builder.

Stuff Still To Do

  • Tests (maybe using MSW as the network is at the heart of this)
  • There's a coercion of number to ResultCodeOf<T, P, M> which I can't seem to get right

License

MIT