How to Build an API SDK That Developers Actually 2026
How to Build an API SDK That Developers Actually Use
A good SDK turns a 50-line HTTP request into a 3-line function call. It handles authentication, retries, pagination, error parsing, and type safety — so developers focus on their product, not your API's quirks. Here's how to build one that developers reach for.
TL;DR
- Authentication in one line is the highest-leverage DX improvement:
new YourAPI({ apiKey })beats manual header management by a mile - Auto-pagination (async iterators) and automatic retries are table stakes — developers expect these without configuring them
- Use Stainless or Speakeasy for SDK generation if you want production-quality code across multiple languages without a dedicated SDK team
- Measure time-to-first-successful-API-call (TTFAC) — Stripe gets developers to a successful call in under 5 minutes; that's your benchmark
- Semantic versioning with in-SDK deprecation warnings keeps existing integrations working while communicating what to migrate to
- Webhook support belongs in the SDK: signature verification and typed event types save developers from a documentation deep-dive
The Good SDK Checklist
1. Authentication Should Be One Line
// ✅ Good — one line setup
const client = new YourAPI({ apiKey: "sk_live_abc123" });
// ❌ Bad — manual header management
const headers = { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" };
fetch("https://api.example.com/users", { headers });
2. Methods Mirror Your API
Resource-based method structure that matches your API documentation:
// Resources map to namespaces
client.users.list()
client.users.get("user_123")
client.users.create({ name: "John", email: "john@example.com" })
client.orders.list({ userId: "user_123" })
3. TypeScript Types (Even for Non-TS SDKs)
Generate types from your OpenAPI spec. TypeScript users get autocomplete, type checking, and inline documentation. Non-TypeScript users benefit from JSDoc types in their editor.
4. Error Handling
Typed errors with specific error classes:
try {
await client.users.create({ email: "invalid" });
} catch (error) {
if (error instanceof YourAPI.ValidationError) {
console.log(error.field); // "email"
console.log(error.code); // "invalid_format"
} else if (error instanceof YourAPI.RateLimitError) {
console.log(error.retryAfter); // 30
} else if (error instanceof YourAPI.AuthenticationError) {
// Re-authenticate
}
}
5. Automatic Pagination
// ✅ Auto-pagination — iterate all results
for await (const user of client.users.list()) {
console.log(user.name);
}
// Also support manual pagination
const page = await client.users.list({ limit: 20 });
console.log(page.data);
const nextPage = await page.nextPage();
6. Automatic Retries
Retry 429s and 5xx errors with exponential backoff. The developer shouldn't need to implement retry logic.
const client = new YourAPI({
apiKey: "sk_live_abc123",
maxRetries: 3, // Default: 2
});
7. Request/Response Logging
const client = new YourAPI({
apiKey: "sk_live_abc123",
debug: true, // Logs request/response for debugging
});
Language Priority
Build SDKs in this order based on developer demand:
| Priority | Language | Why |
|---|---|---|
| 1 | TypeScript/JavaScript | Largest developer population, frontend + backend |
| 2 | Python | Data science, AI/ML, scripting |
| 3 | Go | Cloud infrastructure, microservices |
| 4 | Java/Kotlin | Enterprise, Android |
| 5 | Ruby | Rails ecosystem |
| 6 | PHP | WordPress, Laravel |
| 7 | C#/.NET | Enterprise, Unity |
| 8 | Rust | Systems, WebAssembly |
Start with TypeScript and Python. They cover ~70% of API consumers.
Code Generation vs Hand-Written
Generate From OpenAPI
Tools like openapi-generator, openapi-typescript, and Stainless generate SDKs from your OpenAPI spec.
Pros: Consistent across languages, always in sync with API, less maintenance. Cons: Generated code can feel generic, harder to add SDK-specific ergonomics.
Hand-Written
Stripe, Twilio, and OpenAI hand-write their SDKs for maximum developer experience.
Pros: Best ergonomics, idiomatic per language, custom features. Cons: Expensive to maintain across 5+ languages, can drift from API.
Hybrid (Recommended)
Generate the base client from OpenAPI, then add hand-written ergonomic wrappers on top. Stainless (used by OpenAI) takes this approach — code generation with hand-tuned output.
Documentation
README Must Include
- Installation — copy-paste command (
npm install your-sdk) - Quick start — 5 lines to make a first API call
- Authentication — how to get and use an API key
- Common operations — 3-5 most common use cases with code
- Error handling — how to catch and handle errors
- Link to full docs — reference documentation for all methods
Code Examples > Prose
For every endpoint, show a complete, runnable code example. Developers copy-paste first, read documentation second.
Testing Your SDK
- Unit tests — mock HTTP responses, test parsing, error handling
- Integration tests — hit a sandbox/test environment
- Example apps — build a small app with your SDK to find rough edges
- Developer testing — give the SDK to 3 developers who haven't seen your API, watch them struggle
Stainless and SDK Generation Tools
The SDK generation landscape has matured significantly. Where you once had to choose between generic openapi-generator output and expensive hand-written SDKs, there are now specialized tools that produce production-quality code.
Stainless
Stainless is used by OpenAI, Anthropic, Cloudflare, and Stripe to generate their official SDKs. It generates idiomatic SDKs in TypeScript, Python, Go, Java, Ruby, and C# from your OpenAPI spec. What makes Stainless different from generic generators:
- Generates async iterators for paginated endpoints automatically
- Produces typed error hierarchies from your error response schemas
- Adds streaming support (for SSE/chunked transfer) where applicable
- Generates helper methods for common patterns (file uploads, binary responses)
- Output is readable, maintainable code — not the boilerplate soup of openapi-generator
Stainless is commercial; pricing is based on API volume. For companies where SDKs are a core developer experience investment, it pays for itself by eliminating an SDK maintenance team.
Speakeasy
Speakeasy is a direct Stainless competitor with a similar approach: high-quality code generation from OpenAPI specs, targeting TypeScript, Python, Go, Java, C#, and PHP. Speakeasy puts more emphasis on developer control — you can customize generated code via overlay files rather than modifying the spec itself.
Speakeasy also generates Terraform providers from OpenAPI specs, which matters for infrastructure-as-code integrations.
Fern
Fern takes a different approach: you define your API in Fern's own IDL (which generates OpenAPI), and Fern generates both the server-side types and the client SDKs. This tight integration means the SDK stays in sync with the server implementation, not just the spec. Good choice if you're starting fresh and want everything generated from one source of truth.
openapi-generator
The open-source openapi-generator generates SDKs in 50+ languages. The quality is variable — some generators (Java, TypeScript) are well-maintained; others are community-maintained with gaps. Good for quick-and-cheap SDK generation; not suitable for production SDKs that developers will depend on.
The honest comparison: for a professional API that competes with Stripe-tier developer experience, use Stainless or Speakeasy. For an internal API or a side project, openapi-generator is fine.
Versioning Your SDK
SDK versioning is distinct from API versioning. The SDK version reflects the SDK codebase; a single SDK version can support multiple API versions.
Semantic Versioning
Follow semver strictly:
- Patch (1.0.1): Bug fixes, no interface changes
- Minor (1.1.0): New methods, new optional parameters, new fields on response types
- Major (2.0.0): Breaking changes — removed methods, changed signatures, changed return types
Publish a clear CHANGELOG.md with every release. Developers updating SDKs scan changelogs for breaking changes; make this trivially easy.
Deprecation Warnings in the SDK
Don't silently remove features. Emit deprecation warnings before removing anything:
class YourAPIClient {
/** @deprecated Use client.users.list() instead. Will be removed in v3.0. */
getUsers(options?: ListOptions) {
console.warn(
'[YourAPI] getUsers() is deprecated and will be removed in v3.0. ' +
'Use client.users.list() instead.'
);
return this.users.list(options);
}
}
The warning appears in IDEs as a strikethrough and in runtime console output. Developers see it during development, not in a breaking production deploy.
Give a deprecation window of at least one major version cycle (typically 6-12 months). Announce deprecations in your changelog and documentation.
Supporting Multiple API Versions from One SDK
When you release a new API version, the SDK should support both old and new:
const client = new YourAPI({
apiKey: 'sk_live_abc',
apiVersion: '2026-01-01', // Default to latest
});
// Or explicitly pin to old version
const legacyClient = new YourAPI({
apiKey: 'sk_live_abc',
apiVersion: '2025-06-01',
});
The SDK sends the requested API version via the API-Version header. This lets customers control when they migrate to new API behavior, independent of SDK upgrades. See how to version REST APIs for the API-side versioning strategy.
Webhook Support in SDKs
Webhooks are a pain point for developers. The core problems: verifying signatures (easy to get wrong), parsing event types (tedious without types), and setting up framework middleware (different for every framework). Your SDK should solve all three.
Webhook Signature Verification
// Express middleware — built into the SDK
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['your-api-signature'];
let event;
try {
event = client.webhooks.constructEvent(req.body, sig, process.env.WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// event is fully typed
switch (event.type) {
case 'order.paid':
await handleOrderPaid(event.data); // event.data typed as OrderPaidEvent
break;
case 'user.created':
await handleUserCreated(event.data); // typed as UserCreatedEvent
break;
}
res.json({ received: true });
});
Typed Webhook Events
Generate TypeScript types for every webhook event from your AsyncAPI or OpenAPI spec:
type WebhookEvent =
| { type: 'order.paid'; data: { orderId: string; amount: number; currency: string } }
| { type: 'order.cancelled'; data: { orderId: string; reason: string } }
| { type: 'user.created'; data: { userId: string; email: string } }
| { type: 'subscription.renewed'; data: { subscriptionId: string; nextBillingDate: string } };
Discriminated union types let TypeScript narrow the data type based on event.type. The developer gets autocomplete and type safety for every event's payload.
Next.js and Hono Helpers
// Next.js App Router
export async function POST(request: Request) {
const payload = await request.text();
const sig = request.headers.get('your-api-signature') ?? '';
const event = client.webhooks.constructEvent(payload, sig, process.env.WEBHOOK_SECRET!);
await processEvent(event);
return Response.json({ received: true });
}
// Hono
app.post('/webhooks', async (c) => {
const payload = await c.req.text();
const sig = c.req.header('your-api-signature') ?? '';
const event = client.webhooks.constructEvent(payload, sig, process.env.WEBHOOK_SECRET!);
await processEvent(event);
return c.json({ received: true });
});
Providing framework-specific examples in your documentation eliminates a common integration stumbling block. See building real-time APIs for more on event-driven API patterns.
SDK Publishing
Publishing an SDK correctly means developers can install it with a single command and trust that the version they pin will work reliably.
npm (TypeScript/JavaScript)
{
"name": "your-api",
"version": "1.2.3",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"engines": { "node": ">=18" }
}
Key decisions:
- Dual CJS/ESM: Support both CommonJS and ESM via
exportsto prevent bundler compatibility issues - Provenance: Enable npm provenance (
npm publish --provenance) to link the npm package to the Git commit — increasingly expected for security-conscious teams - Peer dependencies: List Node.js version requirements; don't bundle polyfills unless necessary
PyPI (Python)
# pyproject.toml
[project]
name = "your-api"
version = "1.2.3"
requires-python = ">=3.9"
dependencies = ["httpx>=0.24.0", "pydantic>=2.0"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Use Poetry or Hatch for modern Python packaging. Avoid legacy setup.py. Generate type stubs (.pyi files) or use inline types — Python developers using mypy or Pyright expect them.
CI/CD for SDK Releases
Automate releases to prevent manual errors:
# .github/workflows/publish.yml
name: Publish SDK
on:
push:
tags: ['v*']
jobs:
publish-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci && npm run build && npm test
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Tag releases with v1.2.3. The CI pipeline runs tests, builds, and publishes automatically. Never publish manually — manual releases skip tests.
Developer Onboarding Optimization
The metric that matters most for SDK adoption isn't API coverage or performance — it's time-to-first-successful-API-call (TTFAC). How long does it take a new developer to make their first successful call after landing on your documentation?
Measuring TTFAC
You can measure this in your documentation platform if you emit events when developers copy code snippets. More directly: run user testing sessions. Give five developers who've never used your API 30 minutes and watch where they get stuck. Record it. The friction points will be obvious.
Stripe consistently achieves sub-5-minute TTFAC for their API. The reasons: the quickstart page shows exactly three steps (install, create client, make one call), the sandbox is pre-populated with test data, and errors have clear messages that point to the fix.
What Stripe Gets Right
Stripe's SDK design is the industry benchmark. Specific patterns worth copying:
Test mode vs live mode in the constructor. No separate SDK for test mode — the same client works with both keys, and test mode keys start with sk_test_. Developers can't accidentally use test keys in production.
Predictable error messages. Stripe errors include a message that's human-readable, a code that's machine-readable, and a doc_url that links directly to the relevant documentation. Developers solve their own problems without filing support tickets.
Idempotency by default. The Node.js SDK optionally accepts idempotency keys. The Python SDK adds an idempotency_key parameter to every mutation method. This surfaces the concept to developers in the context where they need it.
Copilot-Friendly Patterns
Most developers now write code with GitHub Copilot or Cursor. SDKs that are copilot-friendly see faster adoption because the AI can suggest correct usage without the developer consulting documentation.
Design for copilot discoverability:
- Descriptive method names:
client.payments.createPaymentIntent()overclient.payments.create() - Rich JSDoc comments: Include parameter descriptions, return type description, and a
@example - Consistent naming conventions: If create methods take
CreateXxxInputobjects, be consistent across all resources - Explicit error types in JSDoc:
@throws {YourAPI.ValidationError}helps copilot suggest correct error handling
/**
* Create a new payment intent.
*
* @param params - Payment configuration
* @param params.amount - Amount in smallest currency unit (cents for USD)
* @param params.currency - ISO 4217 currency code
* @returns A PaymentIntent object
* @throws {YourAPI.ValidationError} If amount or currency is invalid
* @throws {YourAPI.AuthenticationError} If API key is invalid
* @example
* const intent = await client.payments.createPaymentIntent({
* amount: 2000, // $20.00
* currency: 'usd',
* });
*/
async createPaymentIntent(params: CreatePaymentIntentParams): Promise<PaymentIntent> {
return this._post('/payment_intents', params);
}
Good JSDoc is documentation for humans and training signal for AI assistants. Both audiences benefit. For the broader API documentation strategy, see API documentation: OpenAPI vs AsyncAPI.
Conclusion
A great SDK is a developer experience multiplier. Every hour you invest in ergonomics (authentication, pagination, error handling) saves your users cumulative weeks of integration work. Start with the SDK checklist, generate from OpenAPI with Stainless or Speakeasy if volume justifies it, and measure TTFAC as your north-star metric. The SDKs that developers actually use aren't the most feature-complete — they're the ones where the first API call just works.