# standalone-functions
Source: https://speakeasy.com/blog/standalone-functions


import { CodeWithTabs } from "@/mdx/components";
import { Callout } from "@/mdx/components";

Today we're introducing a feature for every Speakeasy-generated TypeScript SDK,
called Standalone Functions. This feature makes it possible for your users to
build leaner apps on top of your API, that can run in the browser, or any other
environment where performance is an important consideration.

## The short version

Rather than needing to import the entire library, your SDK users will be able to
select specific functions they want to use. Here's what the change looks like
with [Dub's SDK](https://dub.co/):

```ts
import { Dub } from "dub";

async function run() {
  const dub = new Dub();

  const count = await dub.links.count();

  console.log("Link count:", count);
}

run();
```

Into this:

```ts
import { DubCore } from "dub/core.js";
import { linksCount } from "dub/funcs/linksCount.js";

async function run() {
  const dub = new DubCore();

  const result = await linksCount(dub);
  if (!result.ok) {
    throw result.error;
  }

  // Use the result
  console.log("Link count:", result.value);
}

run();
```

All the SDK's unused functionality: methods, Zod schemas, encoding/decoding
helpers and so on, will be excluded from the user's final application build. The
bundler will tree-shake the package and handle minification of the unused code
from within the modules.

Additionally, standalone functions return a `Result<Value, KnownErrors>` type
which brings about better error handling ergonomics.

<Callout title="Note" type="info">
  Standalone functions _do not_ replace the existing class-based interface. All
  Speakeasy TypeScript SDKs now provide both functions and classes. We think
  they are both equally valid ways for developers to work with SDKs. The
  decision on which style to use can be based on the project you're building and
  what constraints are in place.
</Callout>

## Impact

Combining standalone functions with ES Modules yields massive savings as shown
in the bundle analysis for the sample programs above.

Using `dub@0.35.0` with the class-based API:

![Bundle size of about 324.4 kilobytes when using Dub v0.35.0 with the class-based API](/assets/blog/standalone-functions/dub-0.35.0-impact.jpeg)

Using `dub@0.36.0` with the standalone functions API:

![Bundle size of about 82.1 kilobytes when using Dub v0.36.0 with the standalone functions API](/assets/blog/standalone-functions/dub-0.36.0-impact.jpeg)

Please note that the sizes above are _before_ compressing the bundles with Gzip
or Brotli. The uncompressed size is a good proxy for metrics like JavaScript
parsing time in the browser. Regardless, it's a great idea to compress code
before serving it to browsers or using a CDN that handles this for you.

<Callout title="Note" type="info">
  Our TypeScript SDKs use Zod to build great support for complex validation and
  (de)serialization logic. If you are already using Zod in your application then
  consider excluding its size (~50KB) when thinking about the numbers above. The
  impact is even more compelling in this case.
</Callout>

## The long version

Many SDKs are built around the concept of a class that you instantiate and,
through it, access various operations:

```ts
import { Beezy } from "@speakeasy-sdks/beezyai";

const beezy = new Beezy(); // The SDK class

const stream = await beezy.chat.stream({
  prompt: "What is the most consumed type of cheese?",
  model: "ex-30b",
});

// Use the result...
```

This works great from a developer experience perspective especially if you are
using an IDE with language server support. You can autocomplete your way to most
functionality and breeze through your work.

If we dig under the surface, we would likely find a typical SDK is arranged like
so:

  <CodeWithTabs
    tabs={[
      {
        label: "src/index.ts",
        language: "typescript",
        code: `import { Chat } from "./chat.js";
  import { Files } from "./files.js";

  export class Beezy extends APIClient {
    chat = new Chat();
    files = new Files();
  }`,
      },
      {
        label: "src/chat.ts",
        language: "typescript",
        code: `import { ChatHistory } from "./chat-history.js";
  import { createEventStream } from "./lib/event-streams.js"
  import { encodeJSON } from "./lib/encodings.js"

  class Chat extends APIClient {
    history = new ChatHistory();

    /**
    * Stream a response to a prompt.
    */
    async stream(req: ChatRequest): Promise<EventStream<ChatCompletion>> { /* ... */ }

    /**
    * Generate a complete response to a prompt.
    */
    async complete(req: CompletionRequest): Promise<ChatResponse> { /* ... */ }
  }`,
      },
      {
        label: "src/files.ts",
        language: "typescript",
        code: `import { encodeMultipart } from "./lib/encodings.js"

  class Files extends APIClient {
    /**
     * Create a multipart request to upload files for fine tuning and other jobs.
     */
    async upload(req: FileUploadRequest): Promise<UploadResult> { /* ... */ }
  }`,
      },
      {
        label: "src/chat-history.ts",
        language: "typescript",
        code: `import { createPaginatedIterable } from "./lib/pagination.js"
  import { encodeJSON } from "./lib/encodings.js"

  class ChatHistory extends APIClient {
    /**
     * Paginated list of chat sessions.
     */
    async function list(req: ListChatsRequest): Promise<ChatSession[]> { /* ... */ }
  }`,
}
]}
/>

However, if you're building a web app/site, there is a downside to this approach
that is not immediately apparent: the entire SDK will be included in your app's
bundle because there is little or no opportunity to tree-shake or exclude unused
code from the SDK. If we were to bundle the snippet of code above, then all the
code in the `Beezy`, `Chat`, `Files` and `ChatHistory` classes as well as all of
the code that supports those classes an their methods, such as pagination and
multipart request helpers, will be include in our app. Yet, we only called
`beezy.chat.stream()`.

### A brief crash course on bundlers and tree-shaking

A lot of web apps are built using front-end frameworks with bundlers such as
[Rollup][rollup], [Webpack][webpack], [ESBuild][esbuild],
[Turbopack][turbopack]. Bundlers are responsible for a bunch of tasks including
taking your TypeScript code and code from all the libraries you used, converting
into a JavaScript files that can be loaded on the browsers. They also employ
techniques to reduce the amount of JavaScript to load such as
[minifying][zod-minify] the code, splitting a bundle into smaller parts to load
functionality incrementally and tree-shaking to eliminate unused code from your
codebase and the libraries you've used.

[rollup]: https://rollupjs.org/
[webpack]: https://webpack.js.org/
[esbuild]: https://esbuild.github.io/
[turbopack]: https://turbo.build/pack/docs
[zod-minify]: /post/writing-zod-code-that-minifies

Tree-shaking is the process of identifying what parts of a JavaScript module
were used and only including that subsection of the module. Here's an example
from ESBuild:

![Example of bundling a simple app with ESBuild](/assets/esbuild-bundle-example.jpeg)

([Playgound link for the screenshot above][esbuild-playground])

[esbuild-playground]: https://esbuild.github.io/try/#YgAwLjIzLjAALS1idW5kbGUgLS1mb3JtYXQ9ZXNtAGUAZW50cnkuanMAaW1wb3J0IHsgZ3JlZXQgfSBmcm9tICIuL21vZHVsZS1hLmpzIjsKCmNvbnNvbGUubG9nKGdyZWV0KCJHZW9yZ2VzIikpOwAAbW9kdWxlLWEuanMAZXhwb3J0IGZ1bmN0aW9uIGdyZWV0KG5hbWUpIHsKICByZXR1cm4gYEhlbGxvLCAke25hbWV9IWA7Cn0KCmV4cG9ydCBmdW5jdGlvbiBhZGQoYSwgYikgewogIHJldHVybiBhICsgYjsKfQoKZXhwb3J0IGNvbnN0IFBJID0gMy4xNDsKCmV4cG9ydCBjb25zdCBjb2xvcnMgPSB7CiAgcmVkOiAiI2ZmMDAwMCIsCiAgZ3JlZW46ICIjMDBmZjAwIiwKICBibHVlOiAiIzAwMDBmZiIsCn07

Notice how in the build output, the `add`, `PI` and `colors` exports were not
included. Most bundlers are capable of tree-shaking, some apply better or more
heuristics than others, but generally the rule is to analyze which module
exports were used and leave out the rest.

### And we're back

So if we understand how tree-shaking works, we can arrange our SDK code a little
differently and greatly reduce the impact of our package on a web app's total
bundle size. This is what's new in our recent changes to the generator. We now
create a folder in every TypeScript SDK at `src/funcs/` and emit standalone
functions. Here's a simplified example of one:

```typescript
import { BeezyCore } from "../core.js";

export async function chatStream(
  client: BeezyCore,
  req: ChatRequest,
): Promise<
  Result<
    EventStream<ChatCompletion>,
    ForbiddenError | RateLimitError | TimeoutError
  >
> { /* ... */ }	
```

The interesting change is that instead of attaching methods to classes, we
designed functions to take a tiny "client" as their first argument. This small
_inversion_ means bundlers can dial it up to the max with their tree-shaking
algorithms since functions are module-level exports.

## Playing the long game

When you peer into a function's source code today, you'll notice it's more
verbose than a one line call to a massive HTTP client abstraction. There's code
to validate data with Zod schemas, encode query, path and header parameters and
execute response matching logic. This was a deliberate decision because it
allows us, and by extension you, to have fine-grained control over tree-shaking.
Whereas deep abstractions are very appealing at first, they end up unnecessarily
dragging in all the functionality an SDK provides even if small subsets of it
are needed. We're choosing shallower abstractions instead and reaping the
benefits.

From the results we've seen so far, we think standalone functions are the right
building block for modern web apps. We're excited to see what you'll build with
them.