
meta-sdk-patterns-eng
by aRustyDev
AI Contexts, Commands, Plugins, Docs, Examples, Specs, Etc
SKILL.md
name: meta-sdk-patterns-eng description: Foundational SDK development patterns that domain-specific SDK skills extend. Use when building SDKs for APIs or specifications, creating SDK development skills, or establishing SDK architecture standards across languages.
SDK Development Patterns
Foundational patterns for building SDKs across languages and domains. This skill serves as:
- Root patterns that domain-specific SDK skills (like
openfeature-sdk-dev) extend - Template for creating new SDK development skills
When to Use This Skill
- Building SDKs that wrap APIs, specifications, or services
- Establishing SDK architecture patterns for a team/organization
- Creating domain-specific SDK development skills
- Reviewing SDK design decisions
- Understanding cross-language SDK patterns
Part 1: SDK Development Fundamentals
SDK Architecture Patterns
Client Structure
SDKs typically expose a central client object that encapsulates configuration, authentication, and API access.
Singleton Pattern
Use when global state is required or resource-intensive initialization.
// TypeScript
class ApiClient {
private static instance: ApiClient;
private constructor(private config: Config) {}
static getInstance(config?: Config): ApiClient {
if (!ApiClient.instance) {
if (!config) throw new Error('Config required for initialization');
ApiClient.instance = new ApiClient(config);
}
return ApiClient.instance;
}
}
# Python
class ApiClient:
_instance = None
def __new__(cls, config=None):
if cls._instance is None:
if config is None:
raise ValueError("Config required for initialization")
cls._instance = super().__new__(cls)
cls._instance._config = config
return cls._instance
// Go
var (
instance *Client
once sync.Once
)
func GetClient(cfg *Config) *Client {
once.Do(func() {
instance = &Client{config: cfg}
})
return instance
}
// Rust
use once_cell::sync::OnceCell;
static CLIENT: OnceCell<Client> = OnceCell::new();
pub fn get_client() -> &'static Client {
CLIENT.get().expect("Client not initialized")
}
pub fn init_client(config: Config) -> Result<(), ClientError> {
CLIENT.set(Client::new(config))
.map_err(|_| ClientError::AlreadyInitialized)
}
// Kotlin
object ApiClient {
private lateinit var config: Config
fun initialize(config: Config) {
this.config = config
}
fun getInstance(): ApiClient {
check(::config.isInitialized) { "Client not initialized" }
return this
}
}
// Swift
class ApiClient {
static let shared = ApiClient()
private var config: Config?
private init() {}
func configure(_ config: Config) {
self.config = config
}
}
// Objective-C
@implementation ApiClient
+ (instancetype)sharedClient {
static ApiClient *sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedClient = [[self alloc] init];
});
return sharedClient;
}
@end
Instance Pattern
Use when multiple configurations are needed or for better testability.
// TypeScript
class ApiClient {
constructor(private config: Config) {}
static create(config: Config): ApiClient {
return new ApiClient(config);
}
}
// Usage
const prodClient = ApiClient.create(prodConfig);
const testClient = ApiClient.create(testConfig);
# Python
class ApiClient:
def __init__(self, config: Config):
self._config = config
@classmethod
def create(cls, config: Config) -> 'ApiClient':
return cls(config)
// Go
type Client struct {
config *Config
http *http.Client
}
func NewClient(cfg *Config) *Client {
return &Client{
config: cfg,
http: &http.Client{Timeout: cfg.Timeout},
}
}
// Rust
pub struct Client {
config: Config,
http: reqwest::Client,
}
impl Client {
pub fn new(config: Config) -> Self {
Self {
http: reqwest::Client::builder()
.timeout(config.timeout)
.build()
.unwrap(),
config,
}
}
}
// Kotlin
class ApiClient private constructor(
private val config: Config
) {
companion object {
fun create(config: Config): ApiClient = ApiClient(config)
}
}
// Swift
class ApiClient {
private let config: Config
init(config: Config) {
self.config = config
}
}
// Objective-C
@implementation ApiClient
- (instancetype)initWithConfig:(Config *)config {
self = [super init];
if (self) {
_config = config;
}
return self;
}
@end
Builder Pattern
Use for complex configuration with many optional parameters.
// TypeScript
class ClientBuilder {
private config: Partial<Config> = {};
withBaseUrl(url: string): this {
this.config.baseUrl = url;
return this;
}
withTimeout(ms: number): this {
this.config.timeout = ms;
return this;
}
withRetries(count: number): this {
this.config.retries = count;
return this;
}
build(): ApiClient {
if (!this.config.baseUrl) {
throw new Error('baseUrl is required');
}
return new ApiClient(this.config as Config);
}
}
// Usage
const client = new ClientBuilder()
.withBaseUrl('https://api.example.com')
.withTimeout(5000)
.withRetries(3)
.build();
# Python
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class ClientBuilder:
_base_url: Optional[str] = None
_timeout: int = 30000
_retries: int = 0
def with_base_url(self, url: str) -> 'ClientBuilder':
self._base_url = url
return self
def with_timeout(self, ms: int) -> 'ClientBuilder':
self._timeout = ms
return self
def with_retries(self, count: int) -> 'ClientBuilder':
self._retries = count
return self
def build(self) -> 'ApiClient':
if not self._base_url:
raise ValueError("base_url is required")
return ApiClient(Config(
base_url=self._base_url,
timeout=self._timeout,
retries=self._retries
))
// Go - Functional Options Pattern (idiomatic)
type Option func(*Client)
func WithTimeout(d time.Duration) Option {
return func(c *Client) {
c.timeout = d
}
}
func WithRetries(n int) Option {
return func(c *Client) {
c.retries = n
}
}
func NewClient(baseURL string, opts ...Option) *Client {
c := &Client{
baseURL: baseURL,
timeout: 30 * time.Second,
retries: 0,
}
for _, opt := range opts {
opt(c)
}
return c
}
// Usage
client := NewClient("https://api.example.com",
WithTimeout(5*time.Second),
WithRetries(3),
)
// Rust - Builder with typestate
pub struct ClientBuilder<S = NoUrl> {
base_url: Option<String>,
timeout: Duration,
retries: u32,
_state: std::marker::PhantomData<S>,
}
pub struct NoUrl;
pub struct HasUrl;
impl ClientBuilder<NoUrl> {
pub fn new() -> Self {
Self {
base_url: None,
timeout: Duration::from_secs(30),
retries: 0,
_state: std::marker::PhantomData,
}
}
pub fn base_url(self, url: impl Into<String>) -> ClientBuilder<HasUrl> {
ClientBuilder {
base_url: Some(url.into()),
timeout: self.timeout,
retries: self.retries,
_state: std::marker::PhantomData,
}
}
}
impl ClientBuilder<HasUrl> {
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = duration;
self
}
pub fn retries(mut self, count: u32) -> Self {
self.retries = count;
self
}
pub fn build(self) -> Client {
Client {
base_url: self.base_url.unwrap(),
timeout: self.timeout,
retries: self.retries,
}
}
}
// Kotlin - DSL Builder
class ClientBuilder {
var baseUrl: String? = null
var timeout: Long = 30000
var retries: Int = 0
fun build(): ApiClient {
requireNotNull(baseUrl) { "baseUrl is required" }
return ApiClient(Config(baseUrl!!, timeout, retries))
}
}
fun apiClient(block: ClientBuilder.() -> Unit): ApiClient {
return ClientBuilder().apply(block).build()
}
// Usage
val client = apiClient {
baseUrl = "https://api.example.com"
timeout = 5000
retries = 3
}
// Swift
class ClientBuilder {
private var baseUrl: String?
private var timeout: TimeInterval = 30
private var retries: Int = 0
func withBaseUrl(_ url: String) -> ClientBuilder {
self.baseUrl = url
return self
}
func withTimeout(_ seconds: TimeInterval) -> ClientBuilder {
self.timeout = seconds
return self
}
func withRetries(_ count: Int) -> ClientBuilder {
self.retries = count
return self
}
func build() throws -> ApiClient {
guard let baseUrl = baseUrl else {
throw ClientError.missingBaseUrl
}
return ApiClient(config: Config(
baseUrl: baseUrl,
timeout: timeout,
retries: retries
))
}
}
// Objective-C
@interface ClientBuilder : NSObject
@property (nonatomic, copy) NSString *baseURL;
@property (nonatomic, assign) NSTimeInterval timeout;
@property (nonatomic, assign) NSInteger retries;
- (instancetype)withBaseURL:(NSString *)url;
- (instancetype)withTimeout:(NSTimeInterval)timeout;
- (instancetype)withRetries:(NSInteger)retries;
- (ApiClient *)build;
@end
@implementation ClientBuilder
- (instancetype)init {
self = [super init];
if (self) {
_timeout = 30.0;
_retries = 0;
}
return self;
}
- (instancetype)withBaseURL:(NSString *)url {
self.baseURL = url;
return self;
}
- (ApiClient *)build {
NSAssert(self.baseURL != nil, @"baseURL is required");
return [[ApiClient alloc] initWithConfig:
[[Config alloc] initWithBaseURL:self.baseURL
timeout:self.timeout
retries:self.retries]];
}
@end
API Design Principles
Resource-Oriented Design
Follow Google's API design patterns with standard methods:
| Method | HTTP | Description |
|---|---|---|
| List | GET /resources | List collection |
| Get | GET /resources/{id} | Get single resource |
| Create | POST /resources | Create new resource |
| Update | PUT/PATCH /resources/{id} | Update existing |
| Delete | DELETE /resources/{id} | Delete resource |
// TypeScript - Resource pattern
interface Resource<T> {
list(options?: ListOptions): Promise<Page<T>>;
get(id: string): Promise<T>;
create(data: CreateInput<T>): Promise<T>;
update(id: string, data: UpdateInput<T>): Promise<T>;
delete(id: string): Promise<void>;
}
class UsersResource implements Resource<User> {
constructor(private client: ApiClient) {}
async list(options?: ListOptions): Promise<Page<User>> {
return this.client.get('/users', { params: options });
}
async get(id: string): Promise<User> {
return this.client.get(`/users/${id}`);
}
// ... other methods
}
// Fluent access
const users = client.users.list({ limit: 10 });
# Python
from abc import ABC, abstractmethod
from typing import Generic, TypeVar, List, Optional
T = TypeVar('T')
class Resource(ABC, Generic[T]):
@abstractmethod
def list(self, **options) -> List[T]: ...
@abstractmethod
def get(self, id: str) -> T: ...
@abstractmethod
def create(self, data: dict) -> T: ...
@abstractmethod
def update(self, id: str, data: dict) -> T: ...
@abstractmethod
def delete(self, id: str) -> None: ...
class UsersResource(Resource[User]):
def __init__(self, client: ApiClient):
self._client = client
def list(self, **options) -> List[User]:
return self._client.get('/users', params=options)
// Go
type Resource[T any] interface {
List(ctx context.Context, opts *ListOptions) (*Page[T], error)
Get(ctx context.Context, id string) (*T, error)
Create(ctx context.Context, input *CreateInput) (*T, error)
Update(ctx context.Context, id string, input *UpdateInput) (*T, error)
Delete(ctx context.Context, id string) error
}
type UsersService struct {
client *Client
}
func (s *UsersService) List(ctx context.Context, opts *ListOptions) (*Page[User], error) {
var page Page[User]
err := s.client.Get(ctx, "/users", opts, &page)
return &page, err
}
// Rust
#[async_trait]
pub trait Resource<T> {
async fn list(&self, options: Option<ListOptions>) -> Result<Page<T>, Error>;
async fn get(&self, id: &str) -> Result<T, Error>;
async fn create(&self, data: CreateInput) -> Result<T, Error>;
async fn update(&self, id: &str, data: UpdateInput) -> Result<T, Error>;
async fn delete(&self, id: &str) -> Result<(), Error>;
}
pub struct UsersResource<'a> {
client: &'a Client,
}
#[async_trait]
impl Resource<User> for UsersResource<'_> {
async fn list(&self, options: Option<ListOptions>) -> Result<Page<User>, Error> {
self.client.get("/users", options).await
}
// ...
}
Options Objects vs Positional Arguments
Prefer options objects for methods with multiple optional parameters.
// TypeScript - Bad: positional optionals
function search(query: string, limit?: number, offset?: number,
sortBy?: string, order?: 'asc' | 'desc'): Promise<Results>;
// Good: options object
interface SearchOptions {
limit?: number;
offset?: number;
sortBy?: string;
order?: 'asc' | 'desc';
}
function search(query: string, options?: SearchOptions): Promise<Results>;
# Python - Use TypedDict or dataclass for options
from typing import TypedDict, Optional
class SearchOptions(TypedDict, total=False):
limit: int
offset: int
sort_by: str
order: str
def search(query: str, options: Optional[SearchOptions] = None) -> Results:
opts = options or {}
# ...
// Go - Options struct with sensible defaults
type SearchOptions struct {
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
SortBy string `json:"sort_by,omitempty"`
Order string `json:"order,omitempty"`
}
func (c *Client) Search(ctx context.Context, query string, opts *SearchOptions) (*Results, error) {
if opts == nil {
opts = &SearchOptions{Limit: 20}
}
// ...
}
// Rust - Builder for complex options
#[derive(Default)]
pub struct SearchOptions {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub sort_by: Option<String>,
pub order: Option<SortOrder>,
}
impl SearchOptions {
pub fn new() -> Self {
Self::default()
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
// ... other builders
}
// Usage
client.search("query", SearchOptions::new().limit(10).offset(0)).await?;
Error Handling
Exception Hierarchies
Create a clear error hierarchy with specific error types.
// TypeScript
export class SdkError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly cause?: Error
) {
super(message);
this.name = 'SdkError';
}
}
export class NetworkError extends SdkError {
constructor(message: string, cause?: Error) {
super(message, 'NETWORK_ERROR', cause);
this.name = 'NetworkError';
}
}
export class ApiError extends SdkError {
constructor(
message: string,
public readonly statusCode: number,
public readonly body?: unknown
) {
super(message, 'API_ERROR');
this.name = 'ApiError';
}
}
export class ValidationError extends SdkError {
constructor(
message: string,
public readonly field?: string
) {
super(message, 'VALIDATION_ERROR');
this.name = 'ValidationError';
}
}
# Python
class SdkError(Exception):
"""Base exception for all SDK errors."""
def __init__(self, message: str, code: str, cause: Exception = None):
super().__init__(message)
self.code = code
self.cause = cause
class NetworkError(SdkError):
"""Network-related errors (connection, timeout)."""
def __init__(self, message: str, cause: Exception = None):
super().__init__(message, 'NETWORK_ERROR', cause)
class ApiError(SdkError):
"""API returned an error response."""
def __init__(self, message: str, status_code: int, body: dict = None):
super().__init__(message, 'API_ERROR')
self.status_code = status_code
self.body = body
class ValidationError(SdkError):
"""Input validation failed."""
def __init__(self, message: str, field: str = None):
super().__init__(message, 'VALIDATION_ERROR')
self.field = field
// Go - Error types with Is/As support
type ErrorCode string
const (
ErrNetwork ErrorCode = "NETWORK_ERROR"
ErrApi ErrorCode = "API_ERROR"
ErrValidation ErrorCode = "VALIDATION_ERROR"
)
type SdkError struct {
Code ErrorCode
Message string
Cause error
}
func (e *SdkError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *SdkError) Unwrap() error {
return e.Cause
}
type ApiError struct {
SdkError
StatusCode int
Body json.RawMessage
}
// Check errors with errors.Is and errors.As
var apiErr *ApiError
if errors.As(err, &apiErr) {
log.Printf("API error %d: %s", apiErr.StatusCode, apiErr.Body)
}
// Rust - thiserror for ergonomic errors
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SdkError {
#[error("Network error: {message}")]
Network {
message: String,
#[source]
source: Option<reqwest::Error>,
},
#[error("API error ({status_code}): {message}")]
Api {
message: String,
status_code: u16,
body: Option<serde_json::Value>,
},
#[error("Validation error: {message}")]
Validation {
message: String,
field: Option<String>,
},
#[error("Configuration error: {0}")]
Config(String),
}
// Usage
match result {
Err(SdkError::Api { status_code: 404, .. }) => {
// Handle not found
}
Err(e) => return Err(e),
Ok(value) => value,
}
// Kotlin
sealed class SdkException(
message: String,
val code: String,
cause: Throwable? = null
) : Exception(message, cause)
class NetworkException(
message: String,
cause: Throwable? = null
) : SdkException(message, "NETWORK_ERROR", cause)
class ApiException(
message: String,
val statusCode: Int,
val body: Any? = null
) : SdkException(message, "API_ERROR")
class ValidationException(
message: String,
val field: String? = null
) : SdkException(message, "VALIDATION_ERROR")
// Usage with when
try {
client.getUser(id)
} catch (e: SdkException) {
when (e) {
is ApiException -> handleApiError(e.statusCode)
is NetworkException -> retry()
is ValidationException -> fixInput(e.field)
}
}
// Swift
enum SdkError: Error {
case network(message: String, underlying: Error?)
case api(message: String, statusCode: Int, body: Data?)
case validation(message: String, field: String?)
case configuration(String)
}
extension SdkError: LocalizedError {
var errorDescription: String? {
switch self {
case .network(let message, _):
return "Network error: \(message)"
case .api(let message, let code, _):
return "API error (\(code)): \(message)"
case .validation(let message, let field):
if let field = field {
return "Validation error on \(field): \(message)"
}
return "Validation error: \(message)"
case .configuration(let message):
return "Configuration error: \(message)"
}
}
}
// Objective-C
extern NSErrorDomain const SdkErrorDomain;
typedef NS_ERROR_ENUM(SdkErrorDomain, SdkErrorCode) {
SdkErrorCodeNetwork = 1000,
SdkErrorCodeApi = 2000,
SdkErrorCodeValidation = 3000,
SdkErrorCodeConfiguration = 4000,
};
// Creating errors
NSError *error = [NSError errorWithDomain:SdkErrorDomain
code:SdkErrorCodeApi
userInfo:@{
NSLocalizedDescriptionKey: @"API returned error",
@"statusCode": @(404),
@"body": responseBody
}];
Retry and Timeout Patterns
// TypeScript
interface RetryConfig {
maxRetries: number;
baseDelay: number;
maxDelay: number;
retryableErrors: string[];
}
async function withRetry<T>(
fn: () => Promise<T>,
config: RetryConfig
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (!isRetryable(error, config.retryableErrors)) {
throw error;
}
if (attempt < config.maxRetries) {
const delay = Math.min(
config.baseDelay * Math.pow(2, attempt),
config.maxDelay
);
await sleep(delay + Math.random() * 100); // Jitter
}
}
}
throw lastError;
}
// Go
type RetryConfig struct {
MaxRetries int
BaseDelay time.Duration
MaxDelay time.Duration
RetryableFunc func(error) bool
}
func WithRetry[T any](ctx context.Context, cfg RetryConfig, fn func() (T, error)) (T, error) {
var lastErr error
var zero T
for attempt := 0; attempt <= cfg.MaxRetries; attempt++ {
result, err := fn()
if err == nil {
return result, nil
}
lastErr = err
if !cfg.RetryableFunc(err) {
return zero, err
}
if attempt < cfg.MaxRetries {
delay := time.Duration(math.Pow(2, float64(attempt))) * cfg.BaseDelay
if delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}
select {
case <-ctx.Done():
return zero, ctx.Err()
case <-time.After(delay):
}
}
}
return zero, lastErr
}
// Rust
use tokio::time::{sleep, Duration};
pub struct RetryConfig {
pub max_retries: u32,
pub base_delay: Duration,
pub max_delay: Duration,
}
impl RetryConfig {
pub async fn execute<T, E, F, Fut>(&self, mut f: F) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
let mut last_error = None;
for attempt in 0..=self.max_retries {
match f().await {
Ok(value) => return Ok(value),
Err(e) => {
last_error = Some(e);
if attempt < self.max_retries {
let delay = self.base_delay * 2u32.pow(attempt);
let delay = delay.min(self.max_delay);
sleep(delay).await;
}
}
}
}
Err(last_error.unwrap())
}
}
Configuration Patterns
Environment-Based Configuration
// TypeScript
interface Config {
baseUrl: string;
apiKey: string;
timeout: number;
debug: boolean;
}
function loadConfig(): Config {
return {
baseUrl: process.env.SDK_BASE_URL ?? 'https://api.example.com',
apiKey: process.env.SDK_API_KEY ?? '',
timeout: parseInt(process.env.SDK_TIMEOUT ?? '30000', 10),
debug: process.env.SDK_DEBUG === 'true',
};
}
# Python
import os
from dataclasses import dataclass
@dataclass
class Config:
base_url: str
api_key: str
timeout: int
debug: bool
@classmethod
def from_env(cls) -> 'Config':
return cls(
base_url=os.getenv('SDK_BASE_URL', 'https://api.example.com'),
api_key=os.getenv('SDK_API_KEY', ''),
timeout=int(os.getenv('SDK_TIMEOUT', '30000')),
debug=os.getenv('SDK_DEBUG', 'false').lower() == 'true',
)
// Go
type Config struct {
BaseURL string `env:"SDK_BASE_URL" envDefault:"https://api.example.com"`
APIKey string `env:"SDK_API_KEY,required"`
Timeout time.Duration `env:"SDK_TIMEOUT" envDefault:"30s"`
Debug bool `env:"SDK_DEBUG" envDefault:"false"`
}
// Using github.com/caarlos0/env
func LoadConfig() (*Config, error) {
cfg := &Config{}
if err := env.Parse(cfg); err != nil {
return nil, err
}
return cfg, nil
}
// Rust - Using envy crate
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Config {
#[serde(default = "default_base_url")]
pub sdk_base_url: String,
pub sdk_api_key: String,
#[serde(default = "default_timeout")]
pub sdk_timeout: u64,
#[serde(default)]
pub sdk_debug: bool,
}
fn default_base_url() -> String {
"https://api.example.com".to_string()
}
fn default_timeout() -> u64 {
30000
}
impl Config {
pub fn from_env() -> Result<Self, envy::Error> {
envy::from_env()
}
}
Authentication Patterns
// TypeScript
interface AuthProvider {
getHeaders(): Promise<Record<string, string>>;
refresh?(): Promise<void>;
}
class ApiKeyAuth implements AuthProvider {
constructor(private apiKey: string) {}
async getHeaders(): Promise<Record<string, string>> {
return { 'Authorization': `Bearer ${this.apiKey}` };
}
}
class OAuth2Auth implements AuthProvider {
private accessToken: string;
private refreshToken: string;
private expiresAt: Date;
constructor(private clientId: string, private clientSecret: string) {}
async getHeaders(): Promise<Record<string, string>> {
if (this.isExpired()) {
await this.refresh();
}
return { 'Authorization': `Bearer ${this.accessToken}` };
}
async refresh(): Promise<void> {
// Token refresh logic
}
private isExpired(): boolean {
return new Date() >= this.expiresAt;
}
}
# Python
from abc import ABC, abstractmethod
from typing import Dict
class AuthProvider(ABC):
@abstractmethod
async def get_headers(self) -> Dict[str, str]:
pass
class ApiKeyAuth(AuthProvider):
def __init__(self, api_key: str):
self._api_key = api_key
async def get_headers(self) -> Dict[str, str]:
return {'Authorization': f'Bearer {self._api_key}'}
class OAuth2Auth(AuthProvider):
def __init__(self, client_id: str, client_secret: str):
self._client_id = client_id
self._client_secret = client_secret
self._access_token = None
self._expires_at = None
async def get_headers(self) -> Dict[str, str]:
if self._is_expired():
await self._refresh()
return {'Authorization': f'Bearer {self._access_token}'}
// Go
type AuthProvider interface {
GetHeaders(ctx context.Context) (http.Header, error)
}
type APIKeyAuth struct {
apiKey string
}
func (a *APIKeyAuth) GetHeaders(ctx context.Context) (http.Header, error) {
h := make(http.Header)
h.Set("Authorization", "Bearer "+a.apiKey)
return h, nil
}
type OAuth2Auth struct {
clientID string
clientSecret string
accessToken string
expiresAt time.Time
mu sync.RWMutex
}
func (a *OAuth2Auth) GetHeaders(ctx context.Context) (http.Header, error) {
a.mu.RLock()
if time.Now().Before(a.expiresAt) {
defer a.mu.RUnlock()
h := make(http.Header)
h.Set("Authorization", "Bearer "+a.accessToken)
return h, nil
}
a.mu.RUnlock()
if err := a.refresh(ctx); err != nil {
return nil, err
}
a.mu.RLock()
defer a.mu.RUnlock()
h := make(http.Header)
h.Set("Authorization", "Bearer "+a.accessToken)
return h, nil
}
// Rust
use async_trait::async_trait;
#[async_trait]
pub trait AuthProvider: Send + Sync {
async fn get_headers(&self) -> Result<HeaderMap, AuthError>;
}
pub struct ApiKeyAuth {
api_key: String,
}
#[async_trait]
impl AuthProvider for ApiKeyAuth {
async fn get_headers(&self) -> Result<HeaderMap, AuthError> {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
format!("Bearer {}", self.api_key).parse()?,
);
Ok(headers)
}
}
pub struct OAuth2Auth {
client_id: String,
client_secret: String,
token: RwLock<Option<TokenData>>,
}
#[async_trait]
impl AuthProvider for OAuth2Auth {
async fn get_headers(&self) -> Result<HeaderMap, AuthError> {
let token = self.token.read().await;
if token.as_ref().map_or(true, |t| t.is_expired()) {
drop(token);
self.refresh().await?;
}
let token = self.token.read().await;
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
format!("Bearer {}", token.as_ref().unwrap().access_token).parse()?,
);
Ok(headers)
}
}
Testing Strategies
Unit Testing with Mocks
// TypeScript - Jest
import { ApiClient } from './client';
import { HttpClient } from './http';
jest.mock('./http');
describe('ApiClient', () => {
let client: ApiClient;
let mockHttp: jest.Mocked<HttpClient>;
beforeEach(() => {
mockHttp = new HttpClient() as jest.Mocked<HttpClient>;
client = new ApiClient({ http: mockHttp });
});
it('should fetch user by id', async () => {
const mockUser = { id: '1', name: 'Test' };
mockHttp.get.mockResolvedValue(mockUser);
const user = await client.users.get('1');
expect(mockHttp.get).toHaveBeenCalledWith('/users/1');
expect(user).toEqual(mockUser);
});
it('should handle errors', async () => {
mockHttp.get.mockRejectedValue(new ApiError('Not found', 404));
await expect(client.users.get('999')).rejects.toThrow(ApiError);
});
});
# Python - pytest
import pytest
from unittest.mock import AsyncMock, patch
@pytest.fixture
def mock_http():
return AsyncMock()
@pytest.fixture
def client(mock_http):
return ApiClient(http=mock_http)
@pytest.mark.asyncio
async def test_get_user(client, mock_http):
mock_user = {'id': '1', 'name': 'Test'}
mock_http.get.return_value = mock_user
user = await client.users.get('1')
mock_http.get.assert_called_with('/users/1')
assert user == mock_user
@pytest.mark.asyncio
async def test_get_user_not_found(client, mock_http):
mock_http.get.side_effect = ApiError('Not found', 404)
with pytest.raises(ApiError) as exc:
await client.users.get('999')
assert exc.value.status_code == 404
// Go - testify
func TestGetUser(t *testing.T) {
mockHTTP := &MockHTTPClient{}
client := NewClient(WithHTTP(mockHTTP))
expectedUser := &User{ID: "1", Name: "Test"}
mockHTTP.On("Get", mock.Anything, "/users/1", mock.Anything).
Return(expectedUser, nil)
user, err := client.Users.Get(context.Background(), "1")
require.NoError(t, err)
assert.Equal(t, expectedUser, user)
mockHTTP.AssertExpectations(t)
}
func TestGetUserNotFound(t *testing.T) {
mockHTTP := &MockHTTPClient{}
client := NewClient(WithHTTP(mockHTTP))
mockHTTP.On("Get", mock.Anything, "/users/999", mock.Anything).
Return(nil, &ApiError{StatusCode: 404})
_, err := client.Users.Get(context.Background(), "999")
var apiErr *ApiError
require.ErrorAs(t, err, &apiErr)
assert.Equal(t, 404, apiErr.StatusCode)
}
// Rust - mockall
use mockall::predicate::*;
#[cfg(test)]
mod tests {
use super::*;
use mockall::mock;
mock! {
HttpClient {}
#[async_trait]
impl HttpClientTrait for HttpClient {
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error>;
}
}
#[tokio::test]
async fn test_get_user() {
let mut mock = MockHttpClient::new();
mock.expect_get::<User>()
.with(eq("/users/1"))
.returning(|_| Ok(User { id: "1".into(), name: "Test".into() }));
let client = Client::new(mock);
let user = client.users().get("1").await.unwrap();
assert_eq!(user.id, "1");
}
}
Contract Testing
// TypeScript - Pact
import { Pact } from '@pact-foundation/pact';
const provider = new Pact({
consumer: 'MySdk',
provider: 'MyApi',
});
describe('API Contract', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
afterEach(() => provider.verify());
it('should get user', async () => {
await provider.addInteraction({
state: 'user exists',
uponReceiving: 'a request for user 1',
withRequest: {
method: 'GET',
path: '/users/1',
},
willRespondWith: {
status: 200,
body: {
id: '1',
name: like('Test User'),
},
},
});
const client = new ApiClient({ baseUrl: provider.mockService.baseUrl });
const user = await client.users.get('1');
expect(user.id).toBe('1');
});
});
Documentation Standards
API Documentation Structure
# SDK Name
Brief description of what the SDK does.
## Installation
### npm
\`\`\`bash
npm install my-sdk
\`\`\`
### yarn
\`\`\`bash
yarn add my-sdk
\`\`\`
## Quick Start
\`\`\`typescript
import { Client } from 'my-sdk';
const client = new Client({ apiKey: 'your-api-key' });
// List users
const users = await client.users.list();
// Get single user
const user = await client.users.get('user-id');
\`\`\`
## Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `string` | required | Your API key |
| `baseUrl` | `string` | `https://api.example.com` | API base URL |
| `timeout` | `number` | `30000` | Request timeout in ms |
| `retries` | `number` | `3` | Number of retry attempts |
## Resources
### Users
#### `client.users.list(options?)`
List all users.
**Parameters:**
- `options.limit` (number, optional): Maximum results to return
- `options.offset` (number, optional): Pagination offset
**Returns:** `Promise<Page<User>>`
**Example:**
\`\`\`typescript
const page = await client.users.list({ limit: 10 });
console.log(page.data);
\`\`\`
## Error Handling
\`\`\`typescript
try {
await client.users.get('invalid-id');
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error ${error.statusCode}: ${error.message}`);
} else if (error instanceof NetworkError) {
console.error('Network error:', error.message);
}
}
\`\`\`
## Migration Guide
### v1.x to v2.x
- `client.getUser(id)` → `client.users.get(id)`
- `UserResponse` type renamed to `User`
Packaging and Distribution
Semantic Versioning
Follow SemVer 2.0.0:
| Version | When to Increment |
|---|---|
| MAJOR (X.0.0) | Breaking changes to public API |
| MINOR (0.X.0) | New backward-compatible features |
| PATCH (0.0.X) | Backward-compatible bug fixes |
Pre-release versions: 1.0.0-alpha.1, 1.0.0-beta.2, 1.0.0-rc.1
Changelog Format
Follow Keep a Changelog:
# Changelog
## [Unreleased]
### Added
- New `client.batch()` method for batch operations
### Changed
- Improved error messages for validation errors
## [2.1.0] - 2024-01-15
### Added
- OAuth2 authentication support
- Automatic token refresh
### Fixed
- Race condition in concurrent requests
## [2.0.0] - 2024-01-01
### Changed
- **BREAKING**: Renamed `getUser()` to `users.get()`
- **BREAKING**: Minimum Node.js version is now 18
### Removed
- **BREAKING**: Removed deprecated `v1` endpoints
Breaking Change Policy
- Document clearly in CHANGELOG and migration guide
- Deprecate first - warn users before removal
- Provide migration path - automated codemods if possible
- Major version only - never in minor/patch
Cross-Cutting Concerns
Logging
// TypeScript
interface Logger {
debug(message: string, meta?: object): void;
info(message: string, meta?: object): void;
warn(message: string, meta?: object): void;
error(message: string, meta?: object): void;
}
class Client {
constructor(
private config: Config,
private logger: Logger = console
) {}
async request(method: string, path: string): Promise<Response> {
this.logger.debug('Making request', { method, path });
try {
const response = await this.http.request(method, path);
this.logger.debug('Request succeeded', { status: response.status });
return response;
} catch (error) {
this.logger.error('Request failed', { error: error.message });
throw error;
}
}
}
Telemetry / Observability
// TypeScript
interface Metrics {
increment(name: string, tags?: Record<string, string>): void;
timing(name: string, ms: number, tags?: Record<string, string>): void;
}
class InstrumentedClient {
constructor(
private client: Client,
private metrics: Metrics
) {}
async request(method: string, path: string): Promise<Response> {
const start = Date.now();
const tags = { method, path };
try {
const response = await this.client.request(method, path);
this.metrics.increment('sdk.request.success', tags);
return response;
} catch (error) {
this.metrics.increment('sdk.request.error', { ...tags, error: error.code });
throw error;
} finally {
this.metrics.timing('sdk.request.duration', Date.now() - start, tags);
}
}
}
Rate Limiting
// TypeScript
class RateLimiter {
private tokens: number;
private lastRefill: number;
constructor(
private maxTokens: number,
private refillRate: number // tokens per second
) {
this.tokens = maxTokens;
this.lastRefill = Date.now();
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens <= 0) {
const waitTime = (1 / this.refillRate) * 1000;
await sleep(waitTime);
this.refill();
}
this.tokens--;
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.maxTokens,
this.tokens + elapsed * this.refillRate
);
this.lastRefill = now;
}
}
Part 2: Creating SDK Development Skills
This section provides patterns for creating domain-specific SDK development skills that extend these foundational patterns.
Skill Structure Template
---
name: <domain>-sdk-dev
description: Develop <Domain> SDK implementations from the specification.
Use when implementing the <Domain> spec in a new language, extending
existing SDKs with new features, or contributing to official SDK repositories.
---
# <Domain> SDK Development
Guide for implementing <Domain> SDKs from the specification.
## When to Use This Skill
- Implementing <Domain> in a new programming language
- Contributing to official <Domain> SDK repositories
- Building custom providers/plugins from scratch
- Extending existing SDKs with new features
## Specification Overview
[Link to official specification]
### Key Concepts
- Concept 1: Brief explanation
- Concept 2: Brief explanation
- ...
## Core Architecture
[Domain-specific architecture diagram]
## Type Definitions
[Domain-specific types with language examples]
## Interface Specifications
[MUST/SHOULD requirements from spec]
## Implementation Checklist
### Phase 1: Core Types
- [ ] Type definition 1
- [ ] Type definition 2
### Phase 2: Core Interface
- [ ] Interface method 1
- [ ] Interface method 2
[Continue phases...]
## Testing Requirements
[Domain-specific test patterns]
## References
- [Official Specification](url)
- [Reference Implementations](url)
- [Test Suites](url)
Analyzing Specifications
When creating an SDK development skill from a specification:
1. Identify Normative Requirements
Look for RFC 2119 keywords:
| Keyword | Meaning | SDK Implication |
|---|---|---|
| MUST | Absolute requirement | Required for compliance |
| MUST NOT | Absolute prohibition | Must be prevented |
| SHOULD | Recommended | Include with escape hatch |
| SHOULD NOT | Not recommended | Warn if used |
| MAY | Optional | Document as optional |
2. Extract Core Concepts
Map specification concepts to SDK components:
Specification Concept → SDK Component
─────────────────────────────────────
Client API → Client class/struct
Provider Interface → Plugin/Provider trait
Configuration → Config struct/builder
Events → Event emitter pattern
Hooks/Middleware → Hook interface
Context → Context object
3. Map Types
Create type definition section mapping spec types to language idioms:
| Spec Type | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| String | string | str | string | String |
| Number | number | int/float | int/float64 | i32/f64 |
| Boolean | boolean | bool | bool | bool |
| Object/Map | Record<K,V> | Dict[K,V] | map[K]V | HashMap<K,V> |
| Array | T[] | List[T] | []T | Vec<T> |
| Optional | T | undefined | Optional[T] | *T | Option<T> |
| Result | Promise<T> | Result[T] | (T, error) | Result<T,E> |
4. Create Implementation Checklist
Break down implementation into phases:
- Core Types - Basic type definitions
- Core Interface - Primary API surface
- Provider/Plugin System - Extensibility
- Lifecycle Management - Init/shutdown
- Error Handling - Error types and propagation
- Events - If spec includes events
- Hooks/Middleware - If spec includes hooks
- Testing - Compliance tests
5. Include Compliance Tests
Reference or include Gherkin/BDD tests from specification:
Feature: Core API
Scenario: Client initialization
Given a valid configuration
When the client is initialized
Then the client status should be "ready"
Scenario: Error handling
Given an invalid configuration
When the client is initialized
Then an error should be returned
And the error code should be "INVALID_CONFIG"
Skill Inheritance Pattern
Domain-specific skills should reference this skill:
---
name: openfeature-sdk-dev
description: ...
---
# OpenFeature SDK Development
> **Note**: This skill extends patterns from `meta-sdk-patterns-eng`.
> See that skill for foundational SDK patterns.
## OpenFeature-Specific Patterns
[Domain-specific content...]
Example: Creating a New SDK Skill
Given a specification for "FeatureFlags API v2":
- Read specification completely
- Extract MUST requirements → Implementation checklist
- Identify types → Type definitions section
- Map interfaces → Interface specifications
- Find test suites → Testing requirements
- Document architecture → Core architecture section
References
API Design
SDK Development
Versioning & Releases
Open Source Best Practices
Language-Specific Resources
- Rust: Rust API Guidelines
- Go: Effective Go
- Python: PEP 8, Google Python Style
- TypeScript: TypeScript Handbook
- Kotlin: Kotlin Coding Conventions
- Swift: Swift API Design Guidelines
- Objective-C: Apple Coding Guidelines
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon