Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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",...
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-12 | ✓→✗ | ▼ Worse | 116% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 177% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 77% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 92% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 109% | 0% |
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.
SDK developer experience (DX) encompasses everything a developer feels when using your library:
Great SDK DX is a competitive advantage. Developers choose tools that make them feel smart.
Review the developer-audience-context skill to understand:
SDK design decisions should flow from deep understanding of your users.
The most frequent use case should require the least code.
Good Design:
python# 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:
python# 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)
Start simple, reveal complexity as needed.
javascript// 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" } });
Catch errors as early as possible, with actionable messages.
Good:
python# 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:
pythonclient = 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
Default values should work for most cases without configuration.
javascript// 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 are documentation. Make them helpful.
Every error message should answer:
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/authenticationBad:
Error: 401 UnauthorizedCreate specific error types that developers can catch:
pythonfrom 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
javascript// 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 is documentation that never goes stale.
typescript// 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 };
Design for IDE autocomplete:
typescript// 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? */ });
typescript// 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 }
Structure your SDK so IDE features help developers:
typescript// 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
typescript/** * 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>
pythondef 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' """
Follow semver strictly:
Breaking changes (require major version bump):
Not breaking (minor or patch):
pythonimport 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()
markdown# 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):**
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]When possible, provide automated migration:
bash# Provide migration scripts npx @myapi/migrate-v3 # Or codemods npx jscodeshift -t @myapi/codemods/v2-to-v3 src/
Python: Use snake_case, context managers, generators
python# 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
javascript// 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
go// 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
Other measured skills in the registry, with their headline benchmark lift.