| name | sdk-design |
| description | Design and build client SDKs for APIs. Outputs idiomatic client libraries with auth handling, retry logic, pagination, error types, and developer experience best practices across Python, TypeScript, and Go. |
| argument-hint | ["API type","target languages","authentication method","distribution channel"] |
| allowed-tools | Read, Write, Bash |
SDK Design
A great SDK makes your API feel native to the language it's written in. It hides auth, retries, pagination, and serialization — so developers write business logic, not plumbing.
Process
- Design the interface first — how would the ideal SDK look from the caller's perspective?
- Generate from OpenAPI/proto — use code generation where possible, customize where it matters.
- Handle auth transparently — credential storage, token refresh, header injection.
- Wrap errors — typed exceptions, not raw HTTP status codes.
- Implement retry logic — exponential backoff with jitter for transient failures.
- Abstract pagination — iterators that hide cursor management.
- Add request/response logging — configurable, scrubs secrets.
- Publish and version — semantic versioning, changelog, migration guides.
- Write examples — real use cases, not just API surface coverage.
Output Format
Python SDK
from .client import AcmeClient
from .models import Order, OrderItem, User
from .exceptions import (
AcmeError, AuthenticationError, RateLimitError,
NotFoundError, ValidationError, ServerError
)
__version__ = "1.4.0"
__all__ = [
"AcmeClient",
"Order", "OrderItem", "User",
"AcmeError", "AuthenticationError", "RateLimitError",
"NotFoundError", "ValidationError", "ServerError",
]
import httpx
import time
import random
import logging
from typing import Iterator, Optional, TypeVar, Generic
from dataclasses import dataclass
from .exceptions import (
AcmeError, AuthenticationError, RateLimitError,
NotFoundError, ValidationError, ServerError
)
from .models import Order, User
from .auth import TokenAuth, ApiKeyAuth
logger = logging.getLogger("acme_sdk")
T = TypeVar("T")
@dataclass
class Page(Generic[T]):
items: list[T]
next_cursor: Optional[str]
has_more: bool
total: Optional[int] = None
class AcmeClient:
"""
Acme API client.
Usage:
# API key auth
client = AcmeClient(api_key="your-key")
# OAuth (auto-refreshes tokens)
client = AcmeClient(
client_id="...",
client_secret="...",
token_url="https://auth.acme.com/token"
)
# Custom base URL (e.g., staging)
client = AcmeClient(api_key="...", base_url="https://staging.api.acme.com")
"""
DEFAULT_BASE_URL = "https://api.acme.com/v1"
DEFAULT_TIMEOUT = 30.0
DEFAULT_MAX_RETRIES = 3
DEFAULT_RETRY_STATUS_CODES = {429, 500, 502, 503, 504}
():
api_key:
._auth = ApiKeyAuth(api_key)
client_id client_secret:
._auth = TokenAuth(client_id, client_secret, token_url)
:
ValueError()
._base_url = (base_url .DEFAULT_BASE_URL).rstrip()
._timeout = timeout .DEFAULT_TIMEOUT
._max_retries = max_retries max_retries .DEFAULT_MAX_RETRIES
._http = http_client httpx.Client(
timeout=._timeout,
headers={: },
follow_redirects=,
)
.orders = OrdersClient()
.users = UsersClient()
.products = ProductsClient()
() -> :
url =
headers = ._auth.get_headers()
last_exception =
attempt (._max_retries + ):
:
response = ._http.request(
method,
url,
params=params,
json=json,
headers=headers,
**kwargs
)
logger.debug(
,
extra={: params, : response.status_code}
)
response.status_code == :
retry_after = (response.headers.get(, ))
attempt < ._max_retries:
time.sleep(retry_after)
RateLimitError(
,
retry_after=retry_after,
response=response
)
response.status_code .DEFAULT_RETRY_STATUS_CODES attempt < ._max_retries:
delay = ( ** attempt * , ) * ( + random.random())
logger.warning()
time.sleep(delay)
._raise_for_status(response)
response.status_code == :
{}
response.json()
(httpx.ConnectError, httpx.TimeoutException) e:
last_exception = e
attempt < ._max_retries:
delay = ( ** attempt * , ) * ( + random.random())
logger.warning()
time.sleep(delay)
AcmeError() e
last_exception:
AcmeError() last_exception
():
response.status_code < :
:
error_data = response.json()
Exception:
error_data = {: response.text}
message = error_data.get(, )
error_code = error_data.get(, )
exceptions = {
: AuthenticationError,
: AuthenticationError,
: NotFoundError,
: ValidationError,
}
exc_class = exceptions.get(response.status_code, ServerError response.status_code >= AcmeError)
exc_class(
message,
status_code=response.status_code,
error_code=error_code,
response=response
)
():
():
._http.close()
():
._http.close()
:
():
._client = client
() -> Order:
headers = {}
idempotency_key:
headers[] = idempotency_key
data = ._client._request(
, ,
json={: user_id, : items},
headers=headers
)
Order(**data)
() -> Order:
data = ._client._request(, )
Order(**data)
() -> Iterator[Order]:
cursor =
:
params = {: page_size}
user_id:
params[] = user_id
status:
params[] = status
cursor:
params[] = cursor
data = ._client._request(, , params=params)
items = [Order(**item) item data.get(, [])]
item items:
item
cursor = data.get()
cursor:
() -> Page[Order]:
params = {: page_size, **filters}
cursor:
params[] = cursor
data = ._client._request(, , params=params)
Page(
items=[Order(**item) item data.get(, [])],
next_cursor=data.get(),
has_more=(data.get()),
total=data.get(),
)
() -> Order:
data = ._client._request(
, ,
json={: reason} reason {}
)
Order(**data)
from typing import Optional
import httpx
class AcmeError(Exception):
"""Base exception for all Acme SDK errors."""
def __init__(
self,
message: str,
status_code: int = None,
error_code: str = None,
response: httpx.Response = None
):
super().__init__(message)
self.message = message
self.status_code = status_code
self.error_code = error_code
self.response = response
def __repr__(self):
return f"{self.__class__.__name__}(message={self.message!r}, status_code={self.status_code})"
class AuthenticationError(AcmeError):
"""Invalid credentials or insufficient permissions."""
class RateLimitError(AcmeError):
"""API rate limit exceeded."""
def __init__(self, message: str, retry_after: int = None, **kwargs):
super().__init__(message, **kwargs)
.retry_after = retry_after
():
():
() -> :
.response:
.response.json().get(, [])
[]
():
TypeScript SDK
import type { Order, User, Page, CreateOrderParams } from './types';
import { AcmeError, AuthError, NotFoundError, RateLimitError, ValidationError } from './errors';
interface ClientOptions {
apiKey?: string;
clientId?: string;
clientSecret?: string;
baseUrl?: string;
timeout?: number;
maxRetries?: number;
}
export class AcmeClient {
private readonly baseUrl: string;
private readonly timeout: number;
private readonly maxRetries: number;
private token: string | null = null;
private tokenExpiry: Date | = ;
: ;
: ;
() {
. = options. ?? ;
. = options. ?? ;
. = options. ?? ;
. = ();
. = ();
}
request<T>(
: ,
: ,
: { ?: <, >; ?: ; ?: <, > } = {}
): <T> {
url = (path.(, ), . + );
(options.) {
( [key, value] .(options.)) {
url..(key, value);
}
}
authHeaders = .();
: | = ;
( attempt = ; attempt <= .; attempt++) {
controller = ();
timeoutId = ( controller.(), .);
{
response = (url.(), {
method,
: {
: ,
: ,
...authHeaders,
...options.,
},
: options. ? .(options.) : ,
: controller.,
});
(timeoutId);
(response. === ) {
retryAfter = (response..() ?? , );
(attempt < .) {
(retryAfter * );
;
}
(, retryAfter);
}
([, , , ].(response.) && attempt < .) {
backoff = .(.(, attempt) * , );
(backoff * ( + .()));
;
}
.(response);
(response. === ) {} T;
response.() <T>;
} (err) {
(timeoutId);
(err ) err;
lastError = err ;
(attempt < .) {
(.(, attempt) * );
}
}
}
();
}
(: ): <> {
(response.) ;
body = response.().( ({ : }));
message = body. ?? ;
(response.) {
: : (message);
: (message);
: (message, body.);
:
(response. >= ) (message, response.);
(message, response.);
}
}
(): <<, >> {
(..) {
{ : .. };
}
{ : };
}
}
{
() {}
(: ): <> {
..<>(, , { : params });
}
(: ): <> {
..<>(, );
}
*(: { ?: ; ?: } = {}): <> {
: | ;
{
page = ..<<>>(, , {
: { ...filters, ...(cursor ? { cursor } : {}) },
});
( item page.) item;
cursor = page. ?? ;
} (cursor);
}
}
(): <> {
( (resolve, ms));
}
Publishing
pip install build twine
python -m build
twine upload dist/*
[project]
name = "acme-sdk"
version = "1.4.0"
requires-python = ">=3.9"
dependencies = ["httpx>=0.25.0"]
npm publish --access public
Rules
- Design the interface before the implementation — write usage examples first.
- One SDK per language — don't share code across languages; idiomatic matters more than DRY.
- Typed return values — return domain objects, not raw dicts.
- Handle token refresh transparently — callers should never deal with expired tokens.
- Wrap pagination with iterators — never make callers manage cursors manually.
- Exponential backoff with jitter — always jitter to prevent thundering herd.
- Typed exceptions —
NotFoundError beats HTTPError(status_code=404).
- Log but don't expose secrets — scrub auth headers from debug logs.
- Semantic versioning — breaking changes require a major version bump.
- Write a changelog — every release documents what changed and why.