Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Design production-ready SDKs with retry logic, error handling, pagination, and multi-language support. Use when building client libraries for APIs or creating developer-facing SDK interfaces.
.claude/skills/ancoleman-designing-sdks/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 144% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 146% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 112% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 111% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 129% | 0% |
Design client libraries (SDKs) with excellent developer experience through intuitive APIs, robust error handling, automatic retries, and consistent patterns across programming languages.
Use when building a client library for a REST API, creating internal service SDKs, implementing retry logic with exponential backoff, handling authentication patterns, creating typed error hierarchies, implementing pagination with async iterators, or designing streaming APIs for real-time data.
Organize SDK code hierarchically:
Client (config: API key, base URL, retries, timeout)
├─ Resources (users, payments, posts)
│ ├─ create(), retrieve(), update(), delete()
│ └─ list() (with pagination)
└─ Top-Level Methods (convenience)Resource-Based (Stripe style):
typescriptconst client = new APIClient({ apiKey: 'sk_test_...' }) const user = await client.users.create({ email: 'user@example.com' })
Use for APIs <100 methods. Prioritizes developer experience.
Command-Based (AWS SDK v3):
typescriptimport { S3Client, PutObjectCommand } from '@aws-sdk/client-s3' await client.send(new PutObjectCommand({ Bucket: '...' }))
Use for APIs >100 methods. Prioritizes bundle size and tree-shaking.
For detailed architectural guidance, see references/architecture-patterns.md.
typescriptconst user = await client.users.create({ email: 'user@example.com' })
All methods return Promises. Avoid callbacks.
python# Sync client = APIClient(api_key='sk_test_...') user = client.users.create(email='user@example.com') # Async async_client = AsyncAPIClient(api_key='sk_test_...') user = await async_client.users.create(email='user@example.com')
Provide both clients. Users choose based on architecture.
goclient := apiclient.New("api_key") user, err := client.Users().Create(ctx, req)
Use context.Context for timeout and cancellation.
typescriptconst client = new APIClient({ apiKey: process.env.API_KEY })
Store keys in environment variables, never hardcode.
typescriptconst client = new APIClient({ clientId: 'id', clientSecret: 'secret', refreshToken: 'token', onTokenRefresh: (newToken) => saveToken(newToken) })
SDK automatically refreshes tokens before expiry.
typescriptawait client.users.list({ headers: { Authorization: `Bearer ${userToken}` } })
Use for multi-tenant applications.
See references/authentication.md for OAuth flows, JWT handling, and credential providers.
typescriptasync function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries: number): Promise<T> { let attempt = 0 while (attempt <= maxRetries) { try { return await fn() } catch (error) { attempt++ if (attempt > maxRetries || !isRetryable(error)) throw error const exponential = Math.min(1000 * Math.pow(2, attempt - 1), 10000) const jitter = Math.random() * 500 await sleep(exponential + jitter) } } } function isRetryable(error: any): boolean { return ( error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || (error.status >= 500 && error.status < 600) || error.status === 429 ) }
Retry Decision Matrix:
| Error Type | Retry? | Rationale | |------------|--------|-----------| | 5xx, 429, Network Timeout | ✅ Yes | Transient errors | | 4xx, 401, 403, 404 | ❌ No | Client errors won't fix themselves |
typescriptif (error.status === 429) { const retryAfter = parseInt(error.headers['retry-after'] || '60') await sleep(retryAfter * 1000) }
Respect Retry-After header on 429 responses.
See references/retry-backoff.md for jitter strategies, circuit breakers, and idempotency keys.
typescriptclass APIError extends Error { constructor( message: string, public status: number, public code: string, public requestId: string ) { super(message) this.name = 'APIError' } } class RateLimitError extends APIError { constructor(message: string, requestId: string, public retryAfter: number) { super(message, 429, 'rate_limit_error', requestId) } } class AuthenticationError extends APIError { constructor(message: string, requestId: string) { super(message, 401, 'authentication_error', requestId) } }
typescripttry { const user = await client.users.create({ email: 'invalid' }) } catch (error) { if (error instanceof RateLimitError) { await sleep(error.retryAfter * 1000) } else if (error instanceof AuthenticationError) { console.error('Invalid API key') } else if (error instanceof APIError) { console.error(`${error.message} (Request ID: ${error.requestId})`) } }
Include request ID in all errors for debugging.
See references/error-handling.md for user-friendly messages, validation errors, and debugging support.
TypeScript:
typescriptfor await (const user of client.users.list({ limit: 100 })) { console.log(user.id, user.email) }
Python:
pythonasync for user in client.users.list(limit=100): print(user.id, user.email)
SDK automatically fetches next page.
typescriptclass UsersResource { async *list(options?: { limit?: number }): AsyncGenerator<User> { let cursor: string | undefined = undefined while (true) { const response = await this.client.request('GET', '/users', { query: { limit: String(options?.limit || 100), ...(cursor ? { cursor } : {}) } }) for (const user of response.data) yield user if (!response.has_more) break cursor = response.next_cursor } } }
typescriptlet cursor: string | undefined = undefined while (true) { const response = await client.users.list({ limit: 100, cursor }) for (const user of response.data) console.log(user.id) if (!response.has_more) break cursor = response.next_cursor }
Provide both automatic and manual options.
See references/pagination.md for cursor vs. offset pagination and Go channel patterns.
typescriptasync *stream(path: string, body?: any): AsyncGenerator<any> { const response = await fetch(url, { headers: { 'Accept': 'text/event-stream' }, body: JSON.stringify(body) }) const reader = response.body!.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value) for (const line of chunk.split('\n')) { if (line.startsWith('data: ')) { const data = line.slice(6) if (data === '[DONE]') return yield JSON.parse(data) } } } } // Usage for await (const chunk of client.posts.stream({ prompt: 'Write a story' })) { process.stdout.write(chunk.content) }
Prevent duplicate operations during retries:
typescriptimport { randomUUID } from 'crypto' if (['POST', 'PATCH', 'PUT'].includes(method)) { headers['Idempotency-Key'] = options?.idempotencyKey || randomUUID() } // Usage await client.charges.create( { amount: 1000 }, { idempotencyKey: 'charge_unique_123' } )
Server deduplicates requests by key.
1.0.0 → 1.1.0: New features (safe)1.1.0 → 2.0.0: Breaking changes (review)1.0.0 → 1.0.1: Bug fixes (safe)typescriptfunction deprecated(message: string, since: string) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value descriptor.value = function (...args: any[]) { console.warn(`[DEPRECATED] ${propertyKey} since ${since}. ${message}`) return originalMethod.apply(this, args) } return descriptor } } @deprecated('Use users.list() instead', 'v2.0.0') async getAll() { return this.list() }
typescriptconst client = new APIClient({ apiKey: 'sk_test_...', apiVersion: '2025-01-01' })
See references/versioning.md for migration strategies.
typescriptinterface ClientConfig { apiKey: string baseURL?: string maxRetries?: number timeout?: number apiVersion?: string onTokenRefresh?: (token: string) => void } class APIClient { constructor(config: ClientConfig) { this.apiKey = config.apiKey this.baseURL = config.baseURL || 'https://api.example.com' this.maxRetries = config.maxRetries ?? 3 this.timeout = config.timeout ?? 30000 } }
Provide sensible defaults, require only apiKey.
| Pattern | Use Case | |---------|----------| | API Key | Service-to-service | | OAuth Refresh | User-based auth | | Bearer Per-Request | Multi-tenant |
| Strategy | Use Case | |----------|----------| | Exponential Backoff | Default retry | | Rate Limit | 429 responses | | Max Retries | Avoid infinite loops (3-5) |
| Pattern | Language | Use Case | |---------|----------|----------| | Async Iterator | TypeScript, Python | Automatic pagination | | Generator | Python | Sync pagination | | Channels | Go | Concurrent iteration | | Manual | All | Explicit control |
Architecture:
references/architecture-patterns.md - Resource vs. command organizationCore Patterns:
references/authentication.md - OAuth, token refresh, credential providersreferences/retry-backoff.md - Exponential backoff, jitter, circuit breakersreferences/error-handling.md - Error hierarchies, debugging supportreferences/pagination.md - Cursor vs. offset, async iteratorsreferences/versioning.md - SemVer, deprecation strategiesreferences/testing-sdks.md - Unit testing, mocking, integration testsTypeScript:
examples/typescript/basic-client.ts - Simple async SDKexamples/typescript/advanced-client.ts - Retry, errors, streamingexamples/typescript/resource-based.ts - Stripe-style organizationPython:
examples/python/sync-client.py - Synchronous clientexamples/python/async-client.py - Async client with asyncioexamples/python/dual-client.py - Both sync and asyncGo:
examples/go/basic-client.go - Simple Go clientexamples/go/context-client.go - Context patternsexamples/go/channel-pagination.go - Channel-based paginationStudy these production SDKs:
TypeScript/JavaScript:
@aws-sdk/client-*): Modular, tree-shakeable, middlewarestripe): Resource-based, typed errors, excellent DXopenai): Streaming, async iterators, modern TypeScriptPython:
boto3): Resource vs. client patterns, paginatorsstripe): Dual sync/async, context managersGo:
github.com/aws/aws-sdk-go-v2): Context, middlewareAvoid these mistakes:
Retry-After header on 429 responsesReview language-specific examples for implementation details. Study references for deep dives on specific patterns. Examine best-in-class SDKs (Stripe, AWS, OpenAI) for inspiration.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | pass→pass | 14,446 | 14,306 | -1% | 1 | 1 | 0% | 2,324 | 5,709 | +146% | 0 | 0 | — |
case-01 | pass→pass | 16,548 | 13,997 | -15% | 1 | 1 | 0% | 2,793 | 5,927 | +112% | 0 | 0 | — |
case-02 | pass→pass | 16,848 | 14,107 | -16% | 1 | 1 | 0% | 2,801 | 5,919 | +111% | 0 | 0 | — |
case-04 | pass→pass | 17,184 | 17,634 | +3% | 1 | 1 | 0% | 2,888 | 6,617 | +129% | 0 | 0 | — |
case-05 | pass→pass | 12,076 | 11,353 | -6% | 1 | 1 | 0% | 2,019 | 5,447 | +170% | 0 | 0 | — |
case-06 | pass→pass | 12,214 | 17,429 | +43% | 1 | 1 | 0% | 1,888 | 6,618 | +251% | 0 | 0 | — |
case-07 | pass→pass | 16,384 | 14,356 | -12% | 1 | 1 | 0% | 2,837 | 6,113 | +115% | 0 | 0 | — |
case-08 | fail→pass | 13,613 | 12,144 | -11% | 1 | 1 | 0% | 2,273 | 5,554 | +144% | 0 | 0 | — |
case-09 | pass→pass | 13,811 | 12,763 | -8% | 1 | 1 | 0% | 2,353 | 5,651 | +140% | 0 | 0 | — |
case-10 | pass→pass | 17,489 | 17,088 | -2% | 1 | 1 | 0% | 3,105 | 6,506 | +110% | 0 | 0 | — |
case-11 | pass→pass | 14,780 | 9,802 | -34% | 1 | 1 | 0% | 2,369 | 5,160 | +118% | 0 | 0 | — |
case-12 | pass→pass | 17,238 | 18,275 | +6% | 1 | 1 | 0% | 2,810 | 6,547 | +133% | 0 | 0 | — |
case-13 | pass→pass | 14,809 | 16,843 | +14% | 1 | 1 | 0% | 2,483 | 6,304 | +154% | 0 | 0 | — |
case-14 | pass→pass | 16,212 | 16,483 | +2% | 1 | 1 | 0% | 2,635 | 6,360 | +141% | 0 | 0 | — |
case-15 | pass→pass | 16,952 | 15,180 | -10% | 1 | 1 | 0% | 2,674 | 6,010 | +125% | 0 | 0 | — |
case-16 | pass→pass | 4,260 | 5,497 | +29% | 1 | 1 | 0% | 792 | 4,478 | +465% | 0 | 0 | — |
case-17 | pass→pass | 12,828 | 12,810 | -0% | 1 | 1 | 0% | 2,212 | 5,701 | +158% | 0 | 0 | — |
case-18 | pass→pass | 15,630 | 10,344 | -34% | 1 | 1 | 0% | 2,677 | 5,154 | +93% | 0 | 0 | — |
case-19 | pass→pass | 12,851 | 8,843 | -31% | 1 | 1 | 0% | 2,174 | 5,012 | +131% | 0 | 0 | — |
case-20 | pass→pass | 19,248 | 17,814 | -7% | 1 | 1 | 0% | 3,545 | 6,787 | +91% | 0 | 0 | — |
case-21 | pass→pass | 18,148 | 18,843 | +4% | 1 | 1 | 0% | 3,276 | 6,790 | +107% | 0 | 0 | — |
case-22 | pass→pass | 13,943 | 19,213 | +38% | 1 | 1 | 0% | 2,943 | 7,538 | +156% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.