Building SDK Wrappers: Creating Developer-Friendly API Clients
By X402 Team | Last Updated: February 2026
Direct Answer
SDK wrappers are libraries that provide a convenient, type-safe interface for interacting with APIs. A good SDK should handle authentication, error handling, retries, pagination, and rate limiting automatically. Key design principles include: fluent/chainable methods, strong typing (TypeScript), comprehensive error handling, automatic retries with exponential backoff, built-in pagination helpers, and thorough documentation. SDKs typically reduce API integration time by 60-80% compared to raw HTTP requests.
Table of Contents
- Why Build an SDK
- SDK Design Principles
- Authentication Handling
- Error Handling
- Pagination
- Testing SDKs
- Publishing SDKs
- SDK Maintenance Best Practices
Why Build an SDK
Developer Experience Benefits
Without SDK (Raw API Calls):
// Manual HTTP request - error-prone and verbose
const response = await fetch('https://api.example.com/users?page=1&limit=50', {
method: 'GET',
headers: {
'Authorization': Bearer ${token},
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'MyApp/1.0'
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
const data = await response.json();
// Need to manually handle pagination, retries, rate limiting...
With SDK (Clean and Simple):
// SDK handles auth, pagination, errors automatically
const users = await client.users.list({ page: 1, limit: 50 });
When to Build an SDK
Build an SDK when:
- Your API has 5+ endpoints
- You have multiple language communities (JavaScript, Python, Ruby, etc.)
- Complex authentication flows (OAuth, JWT refresh)
- API requires specific headers or request formats
- Pagination, rate limiting, or retry logic needed
- You want to improve developer adoption
- Error handling requires specific patterns
Skip SDK when:
- Simple REST API with 2-3 endpoints
- Single-use internal API
- Resource constraints (maintenance overhead)
- API is unstable and changing rapidly
Business Impact
SDKs can significantly impact adoption:
- 60-80% faster integration - Developers get started in minutes vs. hours
- 50% fewer support tickets - SDK handles common issues automatically
- Higher satisfaction - Clean API leads to better developer experience
- Faster time-to-market - Partners can integrate more quickly
SDK Design Principles
1. Principle of Least Surprise
Design APIs that work the way developers expect:
// ✅ GOOD - Predictable, follows conventions
client.users.get(userId);
client.users.list({ limit: 10 });
client.users.create({ name: 'John', email: 'john@example.com' });
client.users.update(userId, { name: 'Jane' });
client.users.delete(userId);
// ❌ BAD - Unpredictable, inconsistent
client.fetchUser(userId);
client.getAllUsers(10);
client.addNewUser({ name: 'John', email: 'john@example.com' });
client.modifyUser(userId, { name: 'Jane' });
client.removeUser(userId);
2. Fluent/Chainable Methods
// ✅ GOOD - Chainable, readable
const users = await client.users
.filter({ role: 'admin' })
.sort('created_at', 'desc')
.limit(50)
.get();
// ❌ BAD - Nested, hard to read
const users = await client.users.get({
filter: { role: 'admin' },
sort: { field: 'created_at', direction: 'desc' },
limit: 50
});
3. Strong Typing (TypeScript)
// SDK with TypeScript provides autocomplete and type safety
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
interface CreateUserParams {
name: string;
email: string;
role?: 'admin' | 'user' | 'guest';
}
class UserResource {
async get(id: string): Promise<User> {
const response = await this.client.request('GET', /users/${id});
return this.deserialize(response);
}
async create(params: CreateUserParams): Promise<User> {
const response = await this.client.request('POST', '/users', params);
return this.deserialize(response);
}
async list(options?: ListOptions): Promise<PaginatedResponse<User>> {
const response = await this.client.request('GET', '/users', options);
return this.deserializePaginated(response);
}
private deserialize(data: any): User {
return {
...data,
createdAt: new Date(data.created_at) // Convert snake_case to camelCase
};
}
}
4. Resource-Based Organization
// ✅ GOOD - Organized by resource
class APIClient {
constructor(apiKey) {
this.users = new UserResource(this);
this.projects = new ProjectResource(this);
this.teams = new TeamResource(this);
}
}
// Usage
const client = new APIClient('api_key_123');
await client.users.get('user_123');
await client.projects.list();
await client.teams.create({ name: 'Engineering' });
// ❌ BAD - Flat namespace, hard to organize
class APIClient {
constructor(apiKey) {
this.apiKey = apiKey;
}
getUser(id) { / ... / }
listUsers() { / ... / }
getProject(id) { / ... / }
listProjects() { / ... / }
getTeam(id) { / ... / }
// 50+ methods in one class...
}
5. Sensible Defaults
class APIClient {
constructor(config) {
this.config = {
baseUrl: config.baseUrl || 'https://api.example.com',
timeout: config.timeout || 30000, // 30 seconds
maxRetries: config.maxRetries || 3,
retryDelay: config.retryDelay || 1000, // 1 second
headers: {
'User-Agent': 'example-sdk-js/1.0.0',
'Accept': 'application/json',
'Content-Type': 'application/json',
...config.headers
}
};
}
}
// Simple initialization with sensible defaults
const client = new APIClient({ apiKey: 'key_123' });
// Advanced users can override
const customClient = new APIClient({
apiKey: 'key_123',
timeout: 60000,
maxRetries: 5,
headers: { 'X-Custom-Header': 'value' }
});
Authentication Handling
API Key Authentication
class APIClient {
constructor(apiKey, options = {}) {
this.apiKey = apiKey;
this.baseUrl = options.baseUrl || 'https://api.example.com';
}
async request(method, path, data = null) {
const url = ${this.baseUrl}${path};
const response = await fetch(url, {
method,
headers: {
'Authorization': Bearer ${this.apiKey},
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : null
});
if (!response.ok) {
throw await this.handleError(response);
}
return response.json();
}
}
// Usage
const client = new APIClient('api_key_xyz123');
OAuth 2.0 with Automatic Token Refresh
class OAuthAPIClient {
constructor(config) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.accessToken = config.accessToken;
this.refreshToken = config.refreshToken;
this.tokenExpiry = config.tokenExpiry || null;
this.baseUrl = config.baseUrl;
// Callback when tokens are refreshed
this.onTokenRefresh = config.onTokenRefresh || (() => {});
}
async request(method, path, data = null) {
// Check if token needs refresh
await this.ensureValidToken();
const url = ${this.baseUrl}${path};
const response = await fetch(url, {
method,
headers: {
'Authorization': Bearer ${this.accessToken},
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : null
});
// If 401, try to refresh token and retry once
if (response.status === 401) {
await this.refreshAccessToken();
return this.request(method, path, data); // Retry once
}
if (!response.ok) {
throw await this.handleError(response);
}
return response.json();
}
async ensureValidToken() {
// Check if token expires in next 5 minutes
if (this.tokenExpiry && Date.now() >= this.tokenExpiry - 300000) {
await this.refreshAccessToken();
}
}
async refreshAccessToken() {
const response = await fetch(${this.baseUrl}/oauth/token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: this.clientId,
client_secret: this.clientSecret,
refresh_token: this.refreshToken
})
});
if (!response.ok) {
throw new Error('Failed to refresh access token');
}
const data = await response.json();
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token || this.refreshToken;
this.tokenExpiry = Date.now() + (data.expires_in 1000);
// Notify application of new tokens
this.onTokenRefresh({
accessToken: this.accessToken,
refreshToken: this.refreshToken,
expiresAt: this.tokenExpiry
});
}
}
// Usage
const client = new OAuthAPIClient({
clientId: 'client_123',
clientSecret: 'secret_abc',
accessToken: 'access_token_xyz',
refreshToken: 'refresh_token_def',
baseUrl: 'https://api.example.com',
onTokenRefresh: (tokens) => {
// Save new tokens to storage
localStorage.setItem('access_token', tokens.accessToken);
localStorage.setItem('refresh_token', tokens.refreshToken);
}
});
Python OAuth Client
import time
import requests
from datetime import datetime, timedelta
class OAuthAPIClient:
def __init__(self, client_id, client_secret, access_token,
refresh_token, base_url, on_token_refresh=None):
self.client_id = client_id
self.client_secret = client_secret
self.access_token = access_token
self.refresh_token = refresh_token
self.base_url = base_url
self.token_expiry = None
self.on_token_refresh = on_token_refresh or (lambda tokens: None)
def request(self, method, path, data=None):
"""Make authenticated API request"""
# Ensure token is valid
self._ensure_valid_token()
url = f"{self.base_url}{path}"
headers = {
'Authorization': f'Bearer {self.access_token}',
'Content-Type': 'application/json'
}
response = requests.request(method, url, json=data, headers=headers)
# Handle token expiration
if response.status_code == 401:
self._refresh_access_token()
return self.request(method, path, data) # Retry once
response.raise_for_status()
return response.json()
def _ensure_valid_token(self):
"""Refresh token if it expires in next 5 minutes"""
if self.token_expiry:
time_until_expiry = self.token_expiry - datetime.now()
if time_until_expiry < timedelta(minutes=5):
self._refresh_access_token()
def _refresh_access_token(self):
"""Refresh OAuth access token"""
response = requests.post(
f"{self.base_url}/oauth/token",
json={
'grant_type': 'refresh_token',
'client_id': self.client_id,
'client_secret': self.client_secret,
'refresh_token': self.refresh_token
}
)
response.raise_for_status()
data = response.json()
self.access_token = data['access_token']
self.refresh_token = data.get('refresh_token', self.refresh_token)
self.token_expiry = datetime.now() + timedelta(seconds=data['expires_in'])
# Notify application
self.on_token_refresh({
'access_token': self.access_token,
'refresh_token': self.refresh_token,
'expires_at': self.token_expiry.isoformat()
})
Error Handling
Custom Error Classes
// Base error class
class APIError extends Error {
constructor(message, statusCode, response) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
this.response = response;
}
}
// Specific error types
class AuthenticationError extends APIError {
constructor(message, response) {
super(message, 401, response);
this.name = 'AuthenticationError';
}
}
class RateLimitError extends APIError {
constructor(message, response, retryAfter) {
super(message, 429, response);
this.name = 'RateLimitError';
this.retryAfter = retryAfter; // Seconds until rate limit resets
}
}
class ValidationError extends APIError {
constructor(message, response, errors) {
super(message, 422, response);
this.name = 'ValidationError';
this.errors = errors; // Field-level validation errors
}
}
class NotFoundError extends APIError {
constructor(message, response) {
super(message, 404, response);
this.name = 'NotFoundError';
}
}
// Error handler
async handleError(response) {
const body = await response.json().catch(() => ({}));
switch (response.status) {
case 401:
case 403:
throw new AuthenticationError(
body.message || 'Authentication failed',
body
);
case 404:
throw new NotFoundError(
body.message || 'Resource not found',
body
);
case 422:
throw new ValidationError(
body.message || 'Validation failed',
body,
body.errors || []
);
case 429:
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
throw new RateLimitError(
body.message || 'Rate limit exceeded',
body,
retryAfter
);
case 500:
case 502:
case 503:
throw new APIError(
body.message || 'Server error',
response.status,
body
);
default:
throw new APIError(
body.message || 'Unknown error',
response.status,
body
);
}
}
// Usage
try {
await client.users.get('invalid_id');
} catch (error) {
if (error instanceof NotFoundError) {
console.log('User not found');
} else if (error instanceof RateLimitError) {
console.log(Rate limited. Retry after ${error.retryAfter} seconds);
} else if (error instanceof ValidationError) {
console.log('Validation errors:', error.errors);
} else {
console.error('API error:', error.message);
}
}
Automatic Retry with Exponential Backoff
class APIClient {
constructor(config) {
this.maxRetries = config.maxRetries || 3;
this.retryDelay = config.retryDelay || 1000; // 1 second
// ... other config
}
async request(method, path, data = null, attempt = 1) {
try {
const response = await fetch(${this.baseUrl}${path}, {
method,
headers: this.getHeaders(),
body: data ? JSON.stringify(data) : null
});
// Don't retry client errors (except 429)
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
throw await this.handleError(response);
}
// Retry server errors and rate limits
if (!response.ok && attempt <= this.maxRetries) {
const delay = this.calculateRetryDelay(attempt, response);
console.log(Request failed, retrying in ${delay}ms (attempt ${attempt}/${this.maxRetries}));
await this.sleep(delay);
return this.request(method, path, data, attempt + 1);
}
if (!response.ok) {
throw await this.handleError(response);
}
return response.json();
} catch (error) {
// Network errors - retry
if (attempt <= this.maxRetries && this.isNetworkError(error)) {
const delay = this.calculateRetryDelay(attempt);
console.log(Network error, retrying in ${delay}ms (attempt ${attempt}/${this.maxRetries}));
await this.sleep(delay);
return this.request(method, path, data, attempt + 1);
}
throw error;
}
}
calculateRetryDelay(attempt, response = null) {
// Use Retry-After header if present (rate limiting)
if (response) {
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
return parseInt(retryAfter) 1000;
}
}
// Exponential backoff with jitter
const exponentialDelay = this.retryDelay Math.pow(2, attempt - 1);
const jitter = Math.random() 1000;
return Math.min(exponentialDelay + jitter, 60000); // Max 60 seconds
}
isNetworkError(error) {
return error.name === 'TypeError' || error.message.includes('network');
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Pagination
Cursor-Based Pagination
class PaginatedResponse {
constructor(data, hasMore, nextCursor) {
this.data = data;
this.hasMore = hasMore;
this.nextCursor = nextCursor;
}
async [Symbol.asyncIterator]() {
// Yield current page
for (const item of this.data) {
yield item;
}
// Auto-fetch next pages
if (this.hasMore && this.fetchNext) {
const nextPage = await this.fetchNext();
for await (const item of nextPage) {
yield item;
}
}
}
}
class UserResource {
async list(options = {}) {
const query = new URLSearchParams({
limit: options.limit || 50,
...(options.cursor && { cursor: options.cursor })
});
const response = await this.client.request('GET', /users?${query});
const paginated = new PaginatedResponse(
response.data,
response.has_more,
response.next_cursor
);
// Allow automatic pagination
if (response.has_more) {
paginated.fetchNext = () => this.list({
...options,
cursor: response.next_cursor
});
}
return paginated;
}
// Convenience method to get all users (auto-paginate)
async listAll(options = {}) {
let cursor = null;
do {
const page = await this.list({ ...options, cursor });
for (const user of page.data) {
yield user;
}
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);
}
}
// Usage examples
// Manual pagination
const firstPage = await client.users.list({ limit: 50 });
console.log(firstPage.data); // First 50 users
if (firstPage.hasMore) {
const secondPage = await client.users.list({
limit: 50,
cursor: firstPage.nextCursor
});
console.log(secondPage.data); // Next 50 users
}
// Automatic pagination with async iterator
const allUsers = [];
for await (const user of client.users.listAll()) {
allUsers.push(user);
// Fetches next page automatically when needed
}
// Or use async iterator on first page
for await (const user of firstPage) {
console.log(user); // Automatically fetches all pages
}
Offset-Based Pagination (Python)
from typing import List, Optional, Iterator
from dataclasses import dataclass
@dataclass
class PaginatedResponse:
data: List
total: int
page: int
per_page: int
total_pages: int
@property
def has_more(self) -> bool:
return self.page < self.total_pages
def __iter__(self) -> Iterator:
"""Allow iterating over current page data"""
return iter(self.data)
class UserResource:
def __init__(self, client):
self.client = client
def list(self, page: int = 1, per_page: int = 50) -> PaginatedResponse:
"""Get paginated list of users"""
response = self.client.request('GET', '/users', {
'page': page,
'per_page': per_page
})
return PaginatedResponse(
data=response['data'],
total=response['total'],
page=response['page'],
per_page=response['per_page'],
total_pages=response['total_pages']
)
def list_all(self, per_page: int = 50) -> Iterator:
"""Auto-paginate through all users"""
page = 1
while True:
result = self.list(page=page, per_page=per_page)
for user in result.data:
yield user
if not result.has_more:
break
page += 1
Usage
Manual pagination
first_page = client.users.list(page=1, per_page=50)
print(f"Total users: {first_page.total}")
print(f"Total pages: {first_page.total_pages}")
for user in first_page:
print(user['name'])
Automatic pagination
all_users = list(client.users.list_all(per_page=100))
print(f"Fetched {len(all_users)} users across all pages")
Testing SDKs
Unit Tests with Mocked HTTP
const nock = require('nock');
const { APIClient } = require('./sdk');
describe('APIClient', () => {
let client;
beforeEach(() => {
client = new APIClient({
apiKey: 'test_key',
baseUrl: 'https://api.example.com'
});
});
afterEach(() => {
nock.cleanAll();
});
describe('users.get()', () => {
test('should fetch user by ID', async () => {
const mockUser = {
id: 'user_123',
name: 'John Doe',
email: 'john@example.com'
};
nock('https://api.example.com')
.get('/users/user_123')
.reply(200, mockUser);
const user = await client.users.get('user_123');
expect(user.id).toBe('user_123');
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@example.com');
});
test('should throw NotFoundError for invalid ID', async () => {
nock('https://api.example.com')
.get('/users/invalid')
.reply(404, { message: 'User not found' });
await expect(client.users.get('invalid'))
.rejects.toThrow('User not found');
});
test('should retry on server error', async () => {
nock('https://api.example.com')
.get('/users/user_123')
.reply(500, { message: 'Server error' })
.get('/users/user_123')
.reply(200, { id: 'user_123', name: 'John Doe' });
const user = await client.users.get('user_123');
expect(user.id).toBe('user_123');
});
});
describe('users.create()', () => {
test('should create new user', async () => {
const newUser = { name: 'Jane Doe', email: 'jane@example.com' };
const createdUser = { id: 'user_456', ...newUser };
nock('https://api.example.com')
.post('/users', newUser)
.reply(201, createdUser);
const user = await client.users.create(newUser);
expect(user.id).toBe('user_456');
expect(user.name).toBe('Jane Doe');
});
test('should throw ValidationError for invalid data', async () => {
nock('https://api.example.com')
.post('/users', {})
.reply(422, {
message: 'Validation failed',
errors: [
{ field: 'name', message: 'Name is required' },
{ field: 'email', message: 'Email is required' }
]
});
await expect(client.users.create({}))
.rejects.toThrow('Validation failed');
});
});
});
Integration Tests
// integration-test.js
const { APIClient } = require('./sdk');
describe('SDK Integration Tests', () => {
let client;
beforeAll(() => {
// Use test API key
client = new APIClient({
apiKey: process.env.TEST_API_KEY,
baseUrl: process.env.TEST_API_URL || 'https://api-test.example.com'
});
});
test('full user lifecycle', async () => {
// Create user
const newUser = await client.users.create({
name: 'Test User',
email: test-${Date.now()}@example.com
});
expect(newUser.id).toBeDefined();
// Get user
const fetchedUser = await client.users.get(newUser.id);
expect(fetchedUser.name).toBe('Test User');
// Update user
const updatedUser = await client.users.update(newUser.id, {
name: 'Updated Name'
});
expect(updatedUser.name).toBe('Updated Name');
// List users (should include our user)
const users = await client.users.list({ limit: 10 });
expect(users.data.some(u => u.id === newUser.id)).toBe(true);
// Delete user
await client.users.delete(newUser.id);
// Verify deletion
await expect(client.users.get(newUser.id))
.rejects.toThrow('User not found');
});
});
Publishing SDKs
Package.json Configuration
{
"name": "@yourcompany/api-sdk",
"version": "1.0.0",
"description": "Official JavaScript SDK for YourCompany API",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc",
"test": "jest",
"prepublishOnly": "npm run build && npm test",
"lint": "eslint src --ext .ts"
},
"keywords": [
"api",
"sdk",
"yourcompany",
"client"
],
"repository": {
"type": "git",
"url": "https://github.com/yourcompany/api-sdk-js"
},
"bugs": {
"url": "https://github.com/yourcompany/api-sdk-js/issues"
},
"homepage": "https://github.com/yourcompany/api-sdk-js#readme",
"author": "YourCompany <support@yourcompany.com>",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.0.0",
"@types/node": "^20.0.0",
"jest": "^29.0.0",
"typescript": "^5.0.0",
"eslint": "^8.0.0"
},
"dependencies": {
"node-fetch": "^3.0.0"
}
}
README Template
# YourCompany API SDK
Official JavaScript/TypeScript SDK for the YourCompany API.
Installation
bash
npm install @yourcompany/api-sdk
or
yarn add @yourcompany/api-sdk
Quick Start
javascript
const { APIClient } = require('@yourcompany/api-sdk');
const client = new APIClient({ apiKey: 'your_api_key' });
// Get a user const user = await client.users.get('user_123');
// Create a project const project = await client.projects.create({ name: 'My Project', description: 'Project description' });
Documentation
Full documentation: https://docs.yourcompany.com/sdk
Authentication
javascript
const client = new APIClient({
apiKey: 'your_api_key',
// Optional configuration
timeout: 30000,
maxRetries: 3
});
Error Handling
javascript
try {
await client.users.get('invalid_id');
} catch (error) {
if (error instanceof NotFoundError) {
console.log('User not found');
} else if (error instanceof RateLimitError) {
console.log(Rate limited. Retry after ${error.retryAfter}s);
}
}
Pagination
javascript
// Manual pagination
const page1 = await client.users.list({ limit: 50 });
if (page1.hasMore) { const page2 = await client.users.list({ cursor: page1.nextCursor }); }
// Automatic pagination for await (const user of client.users.listAll()) { console.log(user); }
Contributing
See CONTRIBUTING.md
License
MIT
Publishing to npm
# 1. Build the package
npm run build
2. Run tests
npm test
3. Update version (patch, minor, or major)
npm version patch
4. Publish to npm
npm publish --access public
5. Push git tags
git push --tags
Publishing to PyPI (Python)
# setup.py
from setuptools import setup, find_packages
with open("README.md", "r") as fh:
long_description = fh.read()
setup(
name="yourcompany-api-sdk",
version="1.0.0",
author="YourCompany",
author_email="support@yourcompany.com",
description="Official Python SDK for YourCompany API",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/yourcompany/api-sdk-python",
packages=find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires='>=3.7',
install_requires=[
"requests>=2.25.0",
],
)
# Build and publish to PyPI
python setup.py sdist bdist_wheel
twine upload dist/
SDK Maintenance Best Practices
1. Semantic Versioning
Follow semantic versioning (MAJOR.MINOR.PATCH):
- MAJOR: Breaking changes (e.g., 1.0.0 → 2.0.0)
- MINOR: New features, backwards-compatible (e.g., 1.0.0 → 1.1.0)
- PATCH: Bug fixes, backwards-compatible (e.g., 1.0.0 → 1.0.1)
2. Changelog Maintenance
# Changelog
[1.2.0] - 2024-03-15
Added
- Added support for webhook signature verification
- New
projects.archive() method
Changed
- Improved error messages for validation failures
- Updated dependencies to latest versions
Fixed
- Fixed pagination cursor bug in
users.listAll()
- Fixed TypeScript type definitions for
create() methods
[1.1.0] - 2024-02-01
Added
- Added automatic token refresh for OAuth clients
- New rate limit handling with exponential backoff
Fixed
- Fixed timeout handling in retry logic
3. Deprecation Warnings
class APIClient {
// Deprecated method
getUserById(id) {
console.warn('DEPRECATED: getUserById() is deprecated. Use users.get() instead.');
return this.users.get(id);
}
// New method
users = {
get: (id) => { / ... */ }
};
}
4. Monitoring SDK Usage
class APIClient {
constructor(config) {
this.telemetry = config.telemetry !== false; // Enabled by default
this.sdkVersion = '1.2.0';
}
getHeaders() {
return {
'User-Agent': yourcompany-sdk-js/${this.sdkVersion},
'X-SDK-Version': this.sdkVersion,
'X-SDK-Language': 'javascript',
...(this.telemetry && { 'X-SDK-Telemetry': 'enabled' })
};
}
}
Summary Checklist
When building an SDK, ensure you:
- [ ] Follow conventions - Use predictable, idiomatic patterns for your language
- [ ] Strong typing - Provide TypeScript definitions or Python type hints
- [ ] Handle auth - Support common patterns (API keys, OAuth with token refresh)
- [ ] Error handling - Custom error classes with helpful messages
- [ ] Automatic retries - Exponential backoff for server errors and rate limits
- [ ] Pagination helpers - Auto-pagination iterators for convenience
- [ ] Comprehensive tests - Unit tests with mocked HTTP and integration tests
- [ ] Great documentation - README with examples, API reference, migration guides
- [ ] Semantic versioning - Follow semver and maintain changelog
- [ ] Monitor usage - Include telemetry headers (opt-out option)
Additional Resources
- Stripe SDK (excellent example): https://github.com/stripe/stripe-node
- Octokit (GitHub SDK): https://github.com/octokit/octokit.js
- AWS SDK: https://github.com/aws/aws-sdk-js-v3
- OpenAPI Generator: https://openapi-generator.tech (auto-generate SDKs)
Tags: SDK development, API client libraries, developer experience, authentication, error handling, pagination, testing, npm publishing, PyPI, TypeScript, Python
Related Guides:
- API Development Best Practices
- Webhook Implementation Guide
- Integration Patterns and Strategies
Start Building with X402
Get our free X402 Implementation Starter Kit with ready-to-use templates, code examples, and best practices.
What is included:
- Quick-start implementation templates
- API integration examples
- Configuration best practices guide