Skill

Design SDKs with exceptional developer experience

Designs SDKs developers love using 4 API design principles, an error-message framework, type-safety patterns, and semver/migration discipline.


74
Spark score
out of 100
Updated 4 months ago
Version 1.0.0

Add to Favorites

Why it matters

Create software development kits that developers love to use by optimizing API design, error messaging, type safety, and IDE integration to drive organic adoption through superior developer experience rather than marketing.

Outcomes

What it gets done

01

Design APIs that optimize for common use cases with progressive disclosure of complexity

02

Write error messages that explain what happened, why it happened, and how to fix it

03

Implement type safety and autocomplete-driven design for IDE integration

04

Structure SDK namespaces and documentation for easy discovery and learning

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/ag-sdk-dx | bash

Overview

SDK Design and Developer Experience

This skill designs SDKs around four API design principles (optimize for the common case, progressive disclosure, fail fast, sensible defaults), a what/why/how error-message framework with distinguishable error types, type-safe autocomplete-friendly interfaces, language-idiom fidelity, and strict semver with structured migration guides. Use it when designing or reviewing an SDK's API surface, error handling, type definitions, versioning, or migration guides - anywhere reducing developer friction is the goal.

What it does

Helps design SDKs that developers love using, on the premise that great developer experience across five stages - discovery, learning, using, debugging, and upgrading - is a competitive advantage that drives adoption better than marketing does. It first points to the developer-audience-context skill to understand target languages, IDE setups, experience level, and competing SDKs, since design decisions should flow from that understanding. Four API design principles anchor everything: optimize for the common case (the most frequent use case needs the least code, with full configuration available but not required); progressive disclosure (simplest usage first, options revealed as needed, full control available at the deepest level); fail fast and clearly (validate at construction time with an actionable error rather than returning None or a generic exception at some later point); and sensible defaults (a client should just work from an API key alone, with automatic retries, reasonable timeouts, and standard auth headers built in). Error messages are treated as documentation: every message should answer what happened, why, and how to fix it, contrasting a bad 401 Unauthorized against a good message naming the exact key issue and giving numbered remediation steps with a docs link. Specific catchable error types (AuthenticationError, AuthorizationError, ValidationError, NotFoundError, RateLimitError, ServerError) let developers handle each failure mode distinctly instead of parsing a generic exception, and errors should carry structured context (field, value, expected format) rather than a bare string. Type safety is framed as documentation that never goes stale: explicit input/output types, discriminated union response types, autocomplete-driven design (so typing client.users. surfaces every operation via namespaced methods), and enum/literal types so the IDE shows valid values instead of accepting any string. Documentation should live in JSDoc/docstrings on every public method with runnable inline examples, since discovery happens in the IDE, not just in a docs site. Versioning follows strict semver, with an explicit breaking-vs-not distinction (removing a method, changing a signature, changing return types, or changing default behavior are breaking; adding methods, adding optional parameters, deprecating without removing, and behavior-correcting bug fixes are not), a deprecation process that warns before removal with a migration link, a structured migration guide template (overview, before/after code per breaking change with a stated reason, removed deprecated features, new features that make migrating worthwhile, and a help channel), and automated migration scripts or codemods when feasible. Making an SDK feel native to its language means following that language's idioms - Python's snake_case, context managers, and generators over camelCase and manual loops; JavaScript's Promises and async/await over callback-based APIs; Go's explicit error returns over exceptions or silent panics - and matching ecosystem conventions for package managers and popular framework integrations. A pre-release quality checklist covers documentation and type coverage on every public API, remediation steps in every error message, tested code examples, an updated changelog, and a migration guide for any breaking change; a separate DX checklist covers a sub-5-minute quickstart, full IDE autocomplete, specifically-catchable errors, transient-failure retry logic, configurable logging, and a debug mode showing request/response detail.

When to use - and when NOT to

Use it when designing or reviewing an SDK's API surface, error handling, type definitions, versioning strategy, or migration guides - anywhere the goal is reducing developer friction and making the SDK feel native to its target language.

Inputs and outputs

Input is an SDK's current or proposed API surface, error handling approach, or a planned breaking change. Output is API design following the four core principles, a structured error-message framework with distinguishable error types, type-safe interfaces with autocomplete support, and (for breaking changes) a semver-correct version bump with a full migration guide.

Integrations

References the developer-audience-context skill for understanding SDK users, and recommends tooling: OpenAPI Generator/Speakeasy/Fern for SDK generation, VCR/WireMock/Pact for HTTP testing, and TypeDoc/Sphinx/GoDoc/YARD for language-specific documentation; hands off to docs-as-marketing, api-onboarding, changelog-updates, and developer-sandbox skills for adjacent work.

Who it's for

SDK and API teams who want their library to feel native to the target language, fail with actionable errors instead of generic exceptions, and handle breaking changes with proper semver and migration guides rather than surprising downstream developers.

Source README

SDK Design and Developer Experience

When to Use

Use this skill when you need design SDKs that developers love to use-APIs that feel native, error messages that guide, and experiences that reduce friction. This skill covers creating SDKs that drive adoption through exceptional developer experience rather than aggressive marketing. Trigger phrases: "SDK design",...

The best SDK marketing is an SDK that developers can't stop talking about. When your SDK makes developers feel productive and competent, they become your advocates. When it frustrates them, no amount of marketing will save you.

Overview

SDK developer experience (DX) encompasses everything a developer feels when using your library:

  • Discovery: How easily can they find and install it?
  • Learning: How quickly can they understand how to use it?
  • Using: How productive are they day-to-day?
  • Debugging: How easily can they fix problems?
  • Upgrading: How painlessly can they adopt new versions?

Great SDK DX is a competitive advantage. Developers choose tools that make them feel smart.

Before You Start

Review the developer-audience-context skill to understand:

  • What languages and frameworks do your target developers use?
  • What IDE/editor setups are most common?
  • What's their experience level with your problem domain?
  • What competing SDKs have they used? What do they like/dislike?

SDK design decisions should flow from deep understanding of your users.

API Design Principles

Principle 1: Optimize for the Common Case

The most frequent use case should require the least code.

Good Design:

### Common case: send a simple message
client.messages.send("Hello world", to="+1234567890")

### Full control when needed
client.messages.send(
    body="Hello world",
    to="+1234567890",
    from_="+0987654321",
    status_callback="https://...",
    media_urls=["https://..."]
)

Bad Design:

### Every call requires full configuration
message = Message(
    body="Hello world",
    to=PhoneNumber("+1234567890"),
    from_=PhoneNumber(config.get_default_from()),
    options=MessageOptions(
        status_callback=None,
        media_urls=[]
    )
)
client.messages.send(message)

Principle 2: Progressive Disclosure

Start simple, reveal complexity as needed.

// Level 1: Simplest possible usage
const result = await client.analyze("Hello world");

// Level 2: Common options
const result = await client.analyze("Hello world", {
  language: "en",
  features: ["sentiment", "entities"]
});

// Level 3: Full control
const result = await client.analyze("Hello world", {
  language: "en",
  features: ["sentiment", "entities"],
  model: "v2-large",
  timeout: 30000,
  retries: { max: 3, backoff: "exponential" }
});

Principle 3: Fail Fast and Clearly

Catch errors as early as possible, with actionable messages.

Good:

### Validation at construction time
client = MyClient(api_key="")
### Raises immediately: ValueError: API key cannot be empty.
### Get your API key at https://dashboard.example.com/keys

### Clear error at runtime
client.users.get("invalid-id")
### Raises: NotFoundError: User 'invalid-id' not found.
### Use client.users.list() to see available users.

Bad:

client = MyClient(api_key="")  # No validation
result = client.users.get("invalid-id")
### Returns: None (is this an error? empty result? who knows?)
### Or worse: raises generic Exception with stack trace

Principle 4: Sensible Defaults

Default values should work for most cases without configuration.

// This should just work without configuration
const client = new MyClient({ apiKey: process.env.MY_API_KEY });

// Sensible defaults:
// - Automatic retries with exponential backoff
// - Reasonable timeouts
// - JSON content type
// - Standard auth headers
// - Connection pooling

Error Messages That Guide

Error messages are documentation. Make them helpful.

The Error Message Framework

Every error message should answer:

  1. What happened?
  2. Why did it happen?
  3. How do I fix it?

Good vs. Bad Error Messages

Good:

AuthenticationError: Invalid API key provided.

The API key 'sk_test_abc...' (test key) cannot be used for
production requests.

To fix this:
1. Go to https://dashboard.example.com/keys
2. Copy your production API key (starts with 'sk_live_')
3. Update your environment variable: MY_API_KEY=sk_live_...

Docs: https://docs.example.com/authentication

Bad:

Error: 401 Unauthorized

Error Types to Distinguish

Create specific error types that developers can catch:

from myapi.errors import (
    AuthenticationError,  # Invalid/missing credentials
    AuthorizationError,   # Valid creds, insufficient permissions
    ValidationError,      # Invalid input data
    NotFoundError,        # Resource doesn't exist
    RateLimitError,       # Too many requests
    ServerError,          # Our fault, retry might help
)

try:
    client.users.get(user_id)
except NotFoundError as e:
    # Handle missing user specifically
except AuthenticationError as e:
    # Handle auth issues specifically
except MyAPIError as e:
    # Catch-all for other API errors

Include Context in Errors

// Bad: generic error
throw new Error("Invalid parameter");

// Good: contextual error
throw new ValidationError({
  message: "Invalid phone number format",
  field: "to",
  value: "+1abc",
  expected: "E.164 format (e.g., +14155551234)",
  docs: "https://docs.example.com/phone-numbers"
});

Type Safety

Type safety is documentation that never goes stale.

TypeScript Best Practices

// Define explicit types for all inputs and outputs
interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
  metadata?: Record<string, unknown>;
}

interface CreateUserInput {
  email: string;
  name: string;
  metadata?: Record<string, unknown>;
}

// Return types are explicit
async function createUser(input: CreateUserInput): Promise<User> {
  // ...
}

// Use discriminated unions for responses
type ApiResponse<T> =
  | { success: true; data: T }
  | { success: false; error: ApiError };

Autocomplete-Driven Design

Design for IDE autocomplete:

// Good: autocomplete shows all options
client.messages.create({
  to: "+1...",     // IDE shows: (property) to: string
  body: "...",    // IDE shows: (property) body: string
  // User types 'me' and sees 'mediaUrls' autocomplete
});

// Bad: requires memorization
client.send("messages", { /* what goes here? */ });

Enum and Literal Types

// Good: constrained values with autocomplete
type MessageStatus = "queued" | "sending" | "sent" | "failed";

interface Message {
  status: MessageStatus;  // IDE shows valid values
}

// Bad: any string accepted
interface Message {
  status: string;  // No guidance, errors at runtime
}

IDE Integration

Make Discovery Easy

Structure your SDK so IDE features help developers:

// Namespace methods logically
client.users.get(id)
client.users.list()
client.users.create(data)
client.users.update(id, data)
client.users.delete(id)

// After typing 'client.users.' the IDE shows all user operations

JSDoc/Docstrings Everywhere

/**
 * Creates a new user in your organization.
 *
 * @param input - The user details
 * @param input.email - Must be a valid email address
 * @param input.name - Display name (max 100 characters)
 * @returns The created user with generated ID
 * @throws {ValidationError} If email format is invalid
 * @throws {ConflictError} If email already exists
 *
 * @example
 * const user = await client.users.create({
 *   email: "jane@example.com",
 *   name: "Jane Developer"
 * });
 */
async createUser(input: CreateUserInput): Promise<User>

Inline Examples

def send_message(self, body: str, to: str, **kwargs) -> Message:
    """
    Send an SMS message.

    Args:
        body: The message content (max 1600 characters)
        to: Recipient phone number in E.164 format

    Returns:
        Message object with ID and status

    Example:
        >>> message = client.messages.send(
        ...     body="Hello from Python!",
        ...     to="+14155551234"
        ... )
        >>> print(message.status)
        'queued'
    """

Versioning Strategy

Semantic Versioning

Follow semver strictly:

  • MAJOR: Breaking changes (removal, signature changes)
  • MINOR: New features (backward compatible)
  • PATCH: Bug fixes (backward compatible)

What Constitutes a Breaking Change

Breaking changes (require major version bump):

  • Removing a public method or property
  • Changing method signatures
  • Changing return types
  • Changing default behavior
  • Removing support for a language/runtime version

Not breaking (minor or patch):

  • Adding new methods
  • Adding optional parameters
  • Deprecating (but not removing) features
  • Bug fixes that change incorrect behavior

Deprecation Process

import warnings

def old_method(self):
    """
    .. deprecated:: 2.3.0
       Use :meth:`new_method` instead. Will be removed in 3.0.0.
    """
    warnings.warn(
        "old_method() is deprecated, use new_method() instead. "
        "See migration guide: https://docs.example.com/migrate-v3",
        DeprecationWarning,
        stacklevel=2
    )
    return self.new_method()

Migration Guides

Migration Guide Structure

### Migrating from v2 to v3

### Overview
Version 3 introduces [major change] and removes [deprecated feature].
Migration typically takes [time estimate].

### Breaking Changes

### 1. Client Initialization
**Before (v2):**
```python
client = MyClient(key="...")

After (v3):

client = MyClient(api_key="...")

Why: Consistency with other SDK parameters.

2. [Next breaking change]

...

Deprecated Features Removed

  • client.old_method() - Use client.new_method() instead
  • LegacyClass - Use ModernClass instead

New Features

  • [Feature that makes migration worthwhile]

Need Help?

  • [Migration support channel]
  • [Office hours for migration questions]

#### Codemods and Automation

When possible, provide automated migration:

```bash
### Provide migration scripts
npx @myapi/migrate-v3

### Or codemods
npx jscodeshift -t @myapi/codemods/v2-to-v3 src/

Making SDKs Feel Native

Language Idioms

Python: Use snake_case, context managers, generators

### Pythonic
with client.batch() as batch:
    for user in client.users.list():
        batch.add(user.send_notification("Hello"))

### Not Pythonic
users = client.getUsers()
batch = client.createBatch()
for i in range(len(users)):
    batch.addOperation(users[i].sendNotification("Hello"))
batch.execute()

JavaScript: Use Promises, async/await, destructuring

// Idiomatic JS
const { data, error } = await client.users.get(id);

// Not idiomatic
client.users.get(id, function(err, result) {
    if (err) { /* callback hell */ }
});

Go: Use error returns, interfaces, channels

// Idiomatic Go
user, err := client.Users.Get(ctx, userID)
if err != nil {
    return fmt.Errorf("getting user: %w", err)
}

// Not idiomatic
user := client.Users.Get(userID)  // panics on error

Match Ecosystem Conventions

  • Use the package manager developers expect (npm, pip, gem, go get)
  • Follow naming conventions of popular libraries in that language
  • Integrate with popular frameworks (Express, Django, Rails)
  • Support popular testing patterns

SDK Quality Checklist

Before Release

  • All public APIs have documentation
  • All public APIs have types (where language supports)
  • Error messages include remediation steps
  • Code examples in docs are tested automatically
  • Changelog is updated with all changes
  • Migration guide for breaking changes
  • Deprecation warnings for removed features

For Great DX

  • Quickstart achieves success in < 5 minutes
  • IDE autocomplete works for all operations
  • Errors are catchable by specific type
  • Retry logic handles transient failures
  • Logging is configurable and useful
  • Debug mode shows request/response details

Tools

SDK Generation

  • OpenAPI Generator: Generate SDKs from OpenAPI specs
  • Swagger Codegen: Alternative generator
  • Speakeasy: Modern SDK generation platform
  • Fern: Type-safe SDK generation

Testing

  • VCR/Betamax: Record and replay HTTP interactions
  • WireMock: Mock HTTP services
  • Pact: Contract testing

Documentation

  • TypeDoc: TypeScript documentation
  • Sphinx: Python documentation
  • GoDoc: Go documentation
  • YARD: Ruby documentation

Related Skills

  • docs-as-marketing: Documentation that showcases SDK capabilities
  • api-onboarding: First experience with your SDK
  • changelog-updates: Communicating SDK changes effectively
  • developer-sandbox: Try SDK without installing
  • developer-audience-context: Understanding SDK users

Limitations

  • Use this skill only when the task clearly matches its upstream source and local project context.
  • Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
  • Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.

FAQ

Common questions

Discussion

Questions & comments · 0

Sign In Sign in to leave a comment.