API Affordances and Discoverability: Design SDKs That Teach Themselves
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:
- 85% of developers succeed without reading docs
- 60% faster time to first API call
- 40% reduction in support tickets
Their Discoverability Principles:
- Naming Follows Mental Models:
paymentIntents.create()
notcreatePI()
- Return Values Guide Next Steps: Objects contain methods for natural next actions
- Errors Suggest Corrections: “Did you mean to use
payment_method
instead ofpaymentMethod
?”
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
- Give your SDK to a developer who’s never seen it
- Watch them use it without documentation
- Note where they get stuck
- Fix those friction points
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
- Stripe API Design Philosophy - Industry gold standard for discoverability
- Designing APIs for Humans - Human-centered API design
- The Psychology of API Design - Cognitive patterns in developer tools