API Affordances and Discoverability: Design SDKs That Teach Themselves

8/13/2025
api · sdk · ux · developer-experience
API Design11 min read8 hours to implement

TL;DR: The best APIs are discovered through exploration, not documentation. Design for curiosity, not memorization.

The $500K Documentation Problem

A payments company spent $500K on API documentation. Usage remained flat.

Then they redesigned their SDK with progressive disclosure—simple defaults that revealed advanced options through autocomplete and type hints.

Result: 300% increase in API adoption. Developers were exploring, not studying.

The Core Insight: APIs Are User Interfaces

Traditional API design focuses on capabilities. Great API design focuses on discoverability.

Think about walking into a well-designed store: you know where to go without asking for directions. Great APIs work the same way.

Mental Model: The Affordance Hierarchy

API Affordance Levels:
1. OBVIOUS: What you can do is immediately clear
2. DISCOVERABLE: Next steps reveal themselves naturally  
3. ESCAPABLE: Advanced use cases don't break the simple ones
4. RECOVERABLE: Mistakes guide you to the right path

Implementation: From Documentation Dependency to Self-Discovery

Progressive Disclosure Pattern

// Level 1: The obvious starting point
const client = createClient({ apiKey: process.env.API_KEY });

// Level 2: Discoverable through autocomplete
const order = await client.orders.create({
  items: [{ sku: "widget_123", quantity: 2 }]
});

// Level 3: Advanced options revealed contextually
const complexOrder = await client.orders.create({
  items: [{ sku: "widget_123", quantity: 2 }],
  shipping: { method: "express", address: {...} },
  // TypeScript suggests available options
  options: {
    idempotencyKey: uuidv4(),
    metadata: { source: "mobile_app" },
    webhookEndpoint: "https://myapp.com/webhooks"
  }
});

// Level 4: Escape hatches for edge cases
const rawResponse = await client.orders.createRaw({
  // Full control when needed, but clearly an escape hatch
  customHeaders: { "X-Custom-Flag": "true" },
  skipValidation: true
});

Self-Documenting Type Design

// Bad: Unclear what's required vs optional
interface CreateUserRequest {
  email: string;
  name: string;
  password: string;
  role: string;
  preferences: object;
}

// Good: Tells a story through types
interface CreateUserRequest {
  // Essential fields - always required
  email: EmailAddress;
  name: string;
  
  // Security - has sensible defaults
  password?: SecurePassword; // Auto-generated if not provided
  
  // Permission - discoverable options
  role?: 'user' | 'admin' | 'readonly';
  
  // Customization - completely optional
  preferences?: UserPreferences;
}

// Type-driven discovery
type EmailAddress = string & { __brand: 'EmailAddress' };
type SecurePassword = string & { 
  __brand: 'SecurePassword';
  minLength: 12;
  requiresSpecialChars: true;
};

// Helper that teaches through usage
function createSecurePassword(input: string): SecurePassword {
  if (input.length < 12) {
    throw new Error('Password must be at least 12 characters. Try using a passphrase like "coffee-mountain-bicycle-42"');
  }
  // ... validation with helpful suggestions
  return input as SecurePassword;
}

Contextual Error Guidance

class DiscoverableError extends Error {
  constructor(
    message: string,
    public readonly suggestion: string,
    public readonly relatedMethods: string[],
    public readonly exampleUsage?: string
  ) {
    super(message);
  }
}

// Usage in SDK
async function getUserOrders(userId: string) {
  if (!userId) {
    throw new DiscoverableError(
      "userId is required",
      "Try calling client.users.list() first to get user IDs",
      ["client.users.list", "client.users.search"],
      `
// Example: Get orders for the first user
const users = await client.users.list();
const orders = await client.orders.getByUser(users[0].id);
      `
    );
  }
  // ... implementation
}

Real-World Excellence: Stripe’s SDK Philosophy

Stripe’s secret: Every method feels like the natural next step from the previous one.

// Feels like a conversation, not a command reference
const paymentIntent = await stripe.paymentIntents.create({
  amount: 2000,
  currency: 'usd',
});

// Next step is obvious from the object returned
await paymentIntent.confirm({
  payment_method: 'pm_card_visa'
});

// Advanced flows discovered contextually
const subscription = await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: 'price_xxx' }],
  // These options revealed through IDE autocomplete
  trial_period_days: 14,
  payment_behavior: 'default_incomplete',
  expand: ['latest_invoice.payment_intent']
});

Impact:

Their Discoverability Principles:

  1. Naming Follows Mental Models: paymentIntents.create() not createPI()
  2. Return Values Guide Next Steps: Objects contain methods for natural next actions
  3. Errors Suggest Corrections: “Did you mean to use payment_method instead of paymentMethod?”

Your Self-Teaching SDK Action Plan

Week 1: Audit Current Discoverability

// Test the "5-minute rule"
// Can a developer accomplish something meaningful in 5 minutes?
const quickWin = await client.somethingObvious();

Week 2: Implement Progressive Disclosure

// Start simple, reveal complexity gradually
interface SimpleConfig {
  apiKey: string;
}

interface AdvancedConfig extends SimpleConfig {
  timeout?: number;
  retries?: number;
  customHeaders?: Record<string, string>;
}

function createClient(config: SimpleConfig): Client;
function createClient(config: AdvancedConfig): AdvancedClient;

Week 3: Add Contextual Guidance

// Errors that teach
class APIError extends Error {
  constructor(
    message: string,
    public readonly hint: string,
    public readonly nextSteps: string[]
  ) {
    super(`${message}\n\nHint: ${hint}\n\nTry: ${nextSteps.join(' or ')}`);
  }
}

Week 4: Test with Fresh Eyes

The Self-Teaching API Checklist

First Success in 5 Minutes: Can someone accomplish something meaningful quickly?
TypeScript Autocomplete Story: Does IDE completion guide users through workflows?
Error Messages as Teaching: Do failures suggest the correct path forward?
Progressive Disclosure: Simple by default, powerful when needed?
Consistent Mental Models: Do similar concepts work similarly across the API?
Escape Hatches: Can power users access full control when needed?

Remember: The best APIs feel like having a pair-programming partner who always knows what you’re trying to do next.

References & Deep Dives