designing-sdks — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited designing-sdks (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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):
const client = new APIClient({ apiKey: 'sk_test_...' })
const user = await client.users.create({ email: '[email protected]' })Use for APIs <100 methods. Prioritizes developer experience.
Command-Based (AWS SDK v3):
import { 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.
const user = await client.users.create({ email: '[email protected]' })All methods return Promises. Avoid callbacks.
# Sync
client = APIClient(api_key='sk_test_...')
user = client.users.create(email='[email protected]')
# Async
async_client = AsyncAPIClient(api_key='sk_test_...')
user = await async_client.users.create(email='[email protected]')Provide both clients. Users choose based on architecture.
client := apiclient.New("api_key")
user, err := client.Users().Create(ctx, req)Use context.Context for timeout and cancellation.
const client = new APIClient({ apiKey: process.env.API_KEY })Store keys in environment variables, never hardcode.
const client = new APIClient({
clientId: 'id',
clientSecret: 'secret',
refreshToken: 'token',
onTokenRefresh: (newToken) => saveToken(newToken)
})SDK automatically refreshes tokens before expiry.
await client.users.list({
headers: { Authorization: `Bearer ${userToken}` }
})Use for multi-tenant applications.
See references/authentication.md for OAuth flows, JWT handling, and credential providers.
async 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 |
if (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.
class 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)
}
}try {
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:
for await (const user of client.users.list({ limit: 100 })) {
console.log(user.id, user.email)
}Python:
async for user in client.users.list(limit=100):
print(user.id, user.email)SDK automatically fetches next page.
class 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
}
}
}let 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.
async *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:
import { 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)function 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() }const client = new APIClient({
apiKey: 'sk_test_...',
apiVersion: '2025-01-01'
})See references/versioning.md for migration strategies.
interface 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.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.