SDKs
Niels Swimberghe
July 14, 2025

How to generate a TypeScript SDK for your API spec

Most developers enjoy building things from scratch—until maintenance and ecosystem complexity catch up with them. Writing a JavaScript library today can be significantly more involved than it was a decade ago. The runtime landscape has broadened from just browsers to include servers, mobile environments, and edge computing platforms. Each of these comes with its own assumptions, APIs, and constraints.

Tooling has improved, but it hasn’t kept pace with the demands of supporting multiple runtimes. If your code needs to run in Node.js, Deno, Bun, React Native, and edge runtimes like Cloudflare Workers or Vercel’s Edge Functions, the support complexity and costs can increase exponentially. Throw in concerns around bundle size, dependency management, and runtime security, and maintaining a handcrafted SDK can easily become a full-time job.

For many teams, the better approach is to generate SDKs from API specifications.

Why Generate TypeScript SDKs Instead of Writing by Hand

Sure, writing an SDK by hand can work—for a while. But as your API evolves, the surface area of potential bugs grows: endpoints change, fields are added, semantics shift, and suddenly your SDK is lying to its users. Perhaps a parameter is marked as optional in code, but it’s actually required. Or a response shape silently changed months ago, but the SDK was never updated. These kinds of mismatches lead to hard-to-diagnose bugs and support headaches.

The root of the problem is duplication. Your API definition exists in two places: once in your backend, and again—manually—across each SDK. That’s not just inefficient; it’s fragile.

Code generation eliminates this duplication. It ensures your SDKs stay aligned with your API by making the specification the single source of truth. Update the spec, regenerate the SDK, and every supported language stays in sync. This drastically reduces drift, prevents subtle bugs, and removes a whole class of copy-paste errors that sneak into handwritten client libraries.

It also scales better—both in terms of engineering effort and runtime correctness. Maintaining SDKs across multiple languages (JavaScript, Python, C#, etc.) is tedious at best and inconsistent at worst. With generation, you get uniform behavior across languages, faster turnaround for changes, and fewer human errors.

Good specs make for good SDKs. That starts with adopting standard formats like OpenAPI (for REST-style JSON APIs), AsyncAPI (for event-driven systems), gRPC (for binary protocols), or OpenRPC/TypeSpec (depending on your architecture). These specs don’t just enable code generation—they unlock documentation, tests, client mocks, and more.

Once you start thinking of your spec as a shared interface contract rather than just documentation, generation becomes more than practical—it becomes foundational.

What To Look For in a TypeScript SDK Generator

Not all generators are created equal. A few can handle the full complexity of a modern API surface, but many fall short in critical areas—especially once you venture outside the happy path of CRUD over JSON. Here’s what to pay close attention to when choosing a TypeScript generator.

✅ Broad Runtime Compatibility

The SDK should run everywhere JavaScript runs: Node.js (including older LTS versions), Deno, Bun, modern browsers, React Native, and edge runtimes like Cloudflare Workers or Vercel Edge Functions. That means ESM and CommonJS output, flexible fetch implementations, and the ability to interop with both browser and server APIs.

You shouldn’t need to fork the SDK or inject polyfills just to get it working in a different environment.

🔒 Zero Dependencies

A generator that produces code with no NPM dependencies is a major win. It:

  • Reduces security risk by eliminating supply chain attack vectors and outdated transitive packages.
  • Shrinks bundle size, which matters in browser and edge environments.
  • Avoids dependency conflicts, especially in monorepos or apps with overlapping libraries.

Every extra dependency is another version mismatch waiting to happen. A zero-dependency SDK sidesteps that problem entirely.

📦 Standards-Based I/O and Types

The output should use platform-native constructs whenever possible — ReadableStream, FormData, and the Fetch API in browsers and Deno; Readable and fs.ReadStream in Node.js. It shouldn’t require adapters or custom wrappers unless absolutely necessary.

And for types: the SDK should generate full, accurate TypeScript definitions directly from your OpenAPI (or equivalent) spec. This includes:

  • Complex nested schemas
  • Discriminated unions and enums
  • Optional vs required fields
  • Generics (when supported by the spec)

Poor type generation leads to developers writing their own type guards or bypassing type safety altogether—defeating the purpose of using TypeScript in the first place.

🧵 Support for Advanced Protocols and Streaming Formats

Many APIs today aren’t just JSON-in, JSON-out. A generator worth using needs to support more than basic HTTP semantics:

  • Server-Sent Events (SSE) — Useful for real-time feedback like LLM token streams or push notifications. The SDK should abstract away the raw event stream and expose a clean async iterator.
  • NDJSON / JSON-L — Line-delimited formats are common for streaming logs or batched data. The SDK should parse and yield individual objects automatically.
  • WebSockets — If your API uses persistent connections, the SDK should manage the connection lifecycle and provide message-level abstractions.
  • Streaming file input/output
  • — Uploading or downloading large files requires support for ReadableStream, Node’s Readable, Blob, and FormData across environments.

These aren’t edge cases anymore—they’re becoming standard requirements. You don’t want to manually wire up stream parsing or implement protocol logic that could be automatically generated and tested.

🧪 Built-in Testing Support

It’s easy to overlook, but generated SDKs should come with some level of testing support. At a minimum:

  • Golden tests to ensure code generation is stable across schema changes.
  • Runtime tests to validate that the SDK’s request/response behaviors match expectations (headers, content types, status codes, etc).

Without tests, it’s too easy for a small change in your schema or generator version to introduce regressions—especially with pagination logic, streaming protocols, or non-standard content types.

Bonus points if the generator can integrate easily into your CI/CD system so that SDK changes are tested and published automatically when the spec changes.

📚 Thorough Documentation and Real-World Examples

Good SDKs are discoverable. Developers shouldn’t have to read the spec to understand how to call your API. That means the generated output should include:

  • Inline JSDoc-style comments
  • A full Markdown README with installation and usage examples
  • An API reference (ideally generated from the spec)
  • Idiomatic code samples for common workflows

Better still if those examples mirror real-world use cases. Connecting to a chat stream, uploading a file, handling pagination—these are the things developers need to see, not just getPetById.

Documentation isn’t fluff; it’s what makes the SDK usable.

Real-World TypeScript SDK Examples

Cohere: Streaming LLM Responses with SSE

Cohere builds enterprise-grade large language models (LLMs) and AI platforms that power chatbots, search, and content generation.

When working with streaming responses using for await, developers can trivially deliver an experience with instant feedback as the model generates text, rather than waiting for the complete response to finish processing.

1const stream = await cohere.chatStream({
2  model: 'command-a-03-2025',
3  messages: [{ role: 'user', content: 'hello world!' }],
4});
5
6
7for await (const chatEvent of stream) {
8  if (chatEvent.type === 'content-delta') {
9    console.log(chatEvent.delta?.message);
10  }
11}

The generated SDK transforms Server-Sent Events (SSE) into an async iterator of parsed objects, abstracting away the stream parsing logic.

ElevenLabs: Audio Streaming In and Out

ElevenLabs builds APIs for speech synthesis, voice cloning, and transcription. Their SDK handles both binary audio input and output, working across environments and stream types.

Convert text to speech and speech to text using the Elevenlabs TypeScript SDK:

1import { ElevenLabsClient, play } from "@elevenlabs/elevenlabs-js";
2import { createReadStream } from "fs";
3
4
5const elevenlabs = new ElevenLabsClient();
6const audioFromText: ReadableStream = await elevenlabs.textToSpeech.convert(
7 "YOUR_TOKEN",
8 {
9   text: "The first move is what sets everything in motion.",
10   modelId: "eleven_multilingual_v2",
11   outputFormat: "mp3_44100_128",
12 }
13);
14
15
16await play(audioFromText);
17
18
19const { text } = await elevenlabs.speechToText.convert({
20 modelId: "scribe_v1",
21 file: createReadStream("audio.mp3"),
22});
23console.log(text);

Sending audio data to Elevenlabs is frictionless because it accepts all file and stream data types a developer may be working with. Whether you pass in a ReadableStream, Blob, Node’s native Readable, or other file-like types, it just works. Elevenlabs returns audio using ReadableStreams and even provides a play function to play the audio on your machine conveniently.

While using buffered types like Blob can be convenient, supporting various streams is essential for memory-efficient applications and for redirecting audio data from microphones and audio to output speakers.

Seamless BigInts in the Square SDK

Square builds commerce and financial services APIs that handle everything from payment processing to inventory management.

Square's backend uses 64-bit integers for all monetary values to maintain consistency with financial industry standards.

Here’s how you can create an order using the Square SDK:

1const orderResponse = await client.orders.create({
2    idempotencyKey: orderUuid,
3    order: {
4        locationId: locationId,
5        lineItems: [
6            {
7                name: "MacBook Pro 16-inch",
8                quantity: "2",
9                basePriceMoney: {
10                    amount: BigInt(249999),
11                    currency: "USD",
12                },
13            },
14            {
15                name: "AppleCare+ Protection",
16                quantity: "2", 
17                basePriceMoney: {
18                    amount: BigInt(39900),
19                    currency: "USD",
20                },
21            },
22        ],
23    },
24});

The generated SDK handles BigInt serialization automatically, converting between JavaScript's BigInt and JSON strings behind the scenes. This is essential since JSON.parse and JSON.stringify don't support BigInt by default, eliminating the need to implement custom serializers while maintaining full TypeScript safety.

Automatic Pagination in the Intercom SDK

Intercom builds customer messaging and support platforms that handle millions of conversations, articles, and user interactions. When working with customer data, such as support articles, conversations, or user lists, APIs typically return paginated results to manage large datasets efficiently.

Here’s how you can paginate over a collection of data using the Intercom SDK:

1import { IntercomClient } from "intercom-client";
2const client = new IntercomClient({ token: "YOUR_TOKEN" });
3const response = await client.articles.list();
4for await (const item of response) {
5    console.log(item);
6}

You could still manually iterate page-by-page:

1const page = await client.articles.list();
2while (page.hasNextPage()) {
3    page = page.getNextPage();
4}

The generated SDK handles pagination automatically with async iteration, eliminating the need to manage page tokens and multiple API calls. Developers can simply use for await to iterate through all results, or take manual control with hasNextPage() and getNextPage() methods when needed. This prevents common pagination bugs while efficiently fetching pages as needed without loading entire datasets into memory.

Get started with SDK Generation

You don’t need to handwrite an SDK to build something developers will actually enjoy using. In fact, doing so often leads to mismatched types, stale documentation, runtime inconsistencies, and long-term maintenance pain.

A well-chosen generator backed by accurate specifications avoids these pitfalls and delivers a better experience across the board—for your users and your team.

The best TypeScript SDKs share several core characteristics:

Broad runtime compatibility — They run unmodified in Node.js, Deno, Bun, browsers, React Native, and edge environments.

Zero external dependencies — Clean output that minimizes bundle size, avoids version conflicts, and drastically reduces security risk.

Strong type fidelity — Accurate and complete TypeScript definitions that reflect your schema and catch mistakes at compile time.

Support for modern transport protocols — Including Server-Sent Events, ND-JSON/JSON-L, WebSockets, and streaming file I/O.

Built-in testing support — Golden tests and runtime tests to ensure SDKs remain stable and correct as your API evolves.

Thorough documentation and real-world examples — Including inline comments, clear usage samples, and idiomatic patterns for real developer workflows.

At Fern, we’ve built our code generation pipeline to check all of these boxes. We generate idiomatic, zero-dependency TypeScript SDKs from OpenAPI, AsyncAPI, gRPC, and other common specifications—and we test them the same way your users will: in real environments, with real data.

If you care about developer experience, cross-runtime compatibility, and drastically reducing the maintenance burden of your API surface, SDK generation isn’t just practical—it’s essential.

Don't just take our word for it, schedule a demo with our team to get a look at our SDK generator in action.

A special thanks to Garrett Serack for contributing to this article.

Niels Swimberghe
July 14, 2025

Get started today

Our team partners with you to launch SDKs and branded API docs that scale to millions of users.