SDKs (Software Development Kits) — Ultimate Deep-Dive Guide
A complete engineering guide from beginner concepts to Staff+/Principal-level SDK architecture, platform engineering, and ecosystem design thinking.
Table of Contents
Open Table of Contents
- 1. Big Picture
- 2. SDK Architecture Deep Dive
- 3. API Design & SDK Contract Design
- 4. Runtime & Environment Deep Dive
- 5. Developer Experience (DX) Engineering
- 6. TypeScript SDK Engineering Deep Dive
- 7. React / Next.js / Astro SDK Integration
- 8. SDK Distribution & Packaging
- 9. SDK Observability & Reliability
- 10. Real-World SDK Case Studies
- 11. Setup Guide
- 12. Tooling Comparison
- 13. Cheatsheet
- 14. Real-World Engineering Mindset
- 15. Brainstorm / Open Questions
- 16. Practice Questions
- 17. Personalized Recommendations
- 18. Official Documentation & Reference Links
- 19. Advanced Engineering Topics
- Summary
1. Big Picture
What SDKs Actually Are
An SDK (Software Development Kit) is a curated abstraction over an API that provides:
- Easy integration — Authentication, configuration, error handling built-in
- Type safety — Compiler-checked contracts instead of string-based API calls
- Developer ergonomics — Autocomplete, documentation, helpful errors
- Runtime abstraction — Works across browsers, Node.js, edge runtimes
- Operational features — Retries, rate limiting, telemetry, offline support
An SDK is NOT:
- Just an HTTP client (that’s too low-level)
- A monolithic kitchen sink (specific to a platform)
- A wrapper around curl (lacks abstraction)
- Framework-specific code in disguise
- Generated API docs (lacks intent)
An SDK IS:
- An abstraction layer (over API + platform)
- A usability layer (optimized for developer happiness)
- A runtime adapter (works across environments)
- A reliability layer (retries, resilience built-in)
- A governance mechanism (enforces best practices)
Why SDKs Exist
┌──────────────────────────────────────────────────────────────┐
│ Problems SDKs Solve │
├──────────────────────────────────────────────────────────────┤
│ │
│ WITHOUT SDK (just API docs): │
│ - Manual HTTP request construction │
│ - Developers learn your API format │
│ - Error handling repeated in every app │
│ - Retry logic duplicated │
│ - Authentication forgotten/misconfigured │
│ - No validation before sending │
│ - Debugging: is my code wrong or API? │
│ - Runtime errors at deployment │
│ - Each language needs different approach │
│ │
│ WITH SDK: │
│ ✓ Import package, start using immediately │
│ ✓ TypeScript catches errors at compile time │
│ ✓ Helpful error messages guide developers │
│ ✓ Retries/resilience handled transparently │
│ ✓ Authentication flows built-in │
│ ✓ Consistent across languages │
│ ✓ Debugging: SDK tells you what's wrong │
│ ✓ Vendor can add features without API changes │
│ │
└──────────────────────────────────────────────────────────────┘
SDK vs Library vs Framework vs CLI
| Component | Purpose | Focus | Lifecycle |
|---|---|---|---|
| SDK | Integrate with remote service | Easy integration + DX | Long-lived with service |
| Library | Provide functionality | Reusability + efficiency | Independent versioning |
| Framework | Structure applications | Architecture + patterns | Opinionated, comprehensive |
| CLI | Command-line interface | Automation + workflows | Often paired with SDK |
| API Client | Call HTTP APIs | Low-level requests | Stateless, basic |
Key distinction: Stripe SDK is an SDK (abstract payment platform). React is a library (reusable UI components). Next.js is a framework (full application structure). npm is a CLI (command-line tool).
SDK Ecosystem Lifecycle
1. API CREATED
Platform exists, needs programmatic access
2. SDK REQUIREMENTS ANALYZED
- What's the core use case?
- What runtimes must we support?
- Who are typical developers?
- What mistakes will they make?
3. ABSTRACTION DESIGNED
- What should be hidden?
- What should be configurable?
- What should be ergonomic?
- What's the API shape?
4. RUNTIME IMPLEMENTATIONS
- Browser SDK
- Node.js SDK
- Edge runtime SDK
- React hooks layer
5. DEVELOPER WORKFLOWS OPTIMIZED
- Documentation written
- Examples created
- TypeScript types refined
- Error messages improved
6. SDK RELEASED
- npm package published
- Version strategy established
- Support channels created
7. ECOSYSTEM ADOPTION
- Developers integrate
- Framework plugins created
- Community best practices emerge
- Feedback loops establish
8. API EVOLUTION MANAGED
- New features added
- SDK evolves alongside API
- Backward compatibility maintained
- Deprecation path established
Real-World SDK Impact
| Company | SDK Investment | Results |
|---|---|---|
| Stripe | Massive (SDKs in 20+ languages) | Dominant payment platform, $95B valuation |
| Firebase | Complete (web, mobile, admin SDKs) | Easy backend for startups, Google acquisition |
| Vercel | Growing (Next.js + deployment APIs) | Preferred deployment platform for React |
| Cloudflare | Strategic (workers + APIs) | Developer-first, competitive moat |
| Supabase | Core (Postgres + realtime SDKs) | 100k+ developers, Firebase alternative |
Pattern: Companies with great SDKs win developer adoption. Developer adoption drives business value.
2. SDK Architecture Deep Dive
SDK Layering Pattern
┌──────────────────────────────────────────────────────────────┐
│ SDK Architecture Layers │
├──────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: Application Code │
│ const result = await client.users.create({ name: "..." })│
│ │ │
│ LAYER 2: High-Level API │
│ ├─ Resource abstraction (users, payments, etc.) │
│ ├─ Method ergonomics (create, update, delete, etc.) │
│ └─ Request building (transforms JS objects → HTTP) │
│ │ │
│ LAYER 3: Middleware Pipeline │
│ ├─ Authentication (add API key/token) │
│ ├─ Serialization (JSON encode) │
│ ├─ Validation (check types, constraints) │
│ ├─ Retry logic (handle transient failures) │
│ └─ Observability (log, metrics, tracing) │
│ │ │
│ LAYER 4: Transport Abstraction │
│ ├─ HTTP client (fetch, Node http, etc.) │
│ ├─ Runtime detection (browser vs Node vs edge) │
│ └─ Request/response transformation │
│ │ │
│ LAYER 5: Runtime Adaptation │
│ ├─ Browser: use Fetch API │
│ ├─ Node.js: use http/https │
│ ├─ Edge: use fetch (Cloudflare Workers, etc.) │
│ └─ Polyfills: fill runtime gaps │
│ │ │
│ LAYER 6: Network │
│ └─ Actual HTTP request to API server │
│ │
└──────────────────────────────────────────────────────────────┘
Request Pipeline Architecture
// How a request flows through SDK layers
const result = await client.users.create({ name: "Alice" });
// Internally:
1. HIGH-LEVEL API (public-facing)
Method: create(data)
↓
2. REQUEST BUILDER
Build HTTP request:
- Endpoint: POST /users
- Headers: { Authorization: "Bearer ..." }
- Body: { name: "Alice" }
↓
3. MIDDLEWARE PIPELINE
- Intercept 1: Validate input schema
- Intercept 2: Add auth header
- Intercept 3: Add request ID
- Intercept 4: Add telemetry headers
↓
4. TRANSPORT
- Choose HTTP client based on runtime
- Send request
- Handle network errors
↓
5. RESPONSE PROCESSING
- Parse JSON
- Validate response schema
- Transform to SDK types
↓
6. RETURN TO USER
result = { id: "123", name: "Alice", ... }
Plugin Architecture
Stripe SDK example (simplified):
// Base SDK
class StripeSDK {
private client: HttpClient;
private plugins: Plugin[] = [];
use(plugin: Plugin): this {
this.plugins.push(plugin);
return this;
}
private async request(method: string, path: string, data?: any) {
let request = { method, path, data };
// Run through plugins
for (const plugin of this.plugins) {
request = await plugin.onBeforeRequest?.(request) || request;
}
const response = await this.client.send(request);
for (const plugin of this.plugins) {
response = await plugin.onAfterResponse?.(response) || response;
}
return response;
}
}
// Usage
const stripe = new StripeSDK();
stripe.use(authPlugin); // Add authentication
stripe.use(retryPlugin); // Add retry logic
stripe.use(loggingPlugin); // Add logging
stripe.use(telemetryPlugin); // Add metrics
3. API Design & SDK Contract Design
API Shape Matters
The shape of an SDK’s API determines developer experience:
// Option A: Method-based (procedural)
const user = await stripe.users.create({ name: "Alice" });
const updated = await stripe.users.update(user.id, { name: "Bob" });
// Option B: Object-based (OOP)
const user = new User({ name: "Alice" });
user.save();
user.name = "Bob";
user.save();
// Option C: Functional (composable)
const user = pipe(
createUser({ name: "Alice" }),
updateUser({ name: "Bob" }),
);
// Option D: Builder pattern (fluent)
const user = await User.builder()
.withName("Alice")
.create()
.then(u => u.setName("Bob"))
.save();
// Each has trade-offs:
// A (method-based): Simple, HTTP-like, consistent with REST
// B (OOP): Familiar to some, state management challenges
// C (functional): Powerful, learning curve
// D (builder): Readable chains, verbose
Versioning & Evolution
SDK versioning challenges:
PROBLEM: API changes, SDK must adapt
OPTIONS:
1. Major version per API version
SDK 1.x → API v1
SDK 2.x → API v2
Breaking: Users must update SDK
Pro: Clear separation
Con: Forced updates
2. Single SDK, multiple API versions
SDK 4.x works with API v1, v2, v3
Breaking: None (backward compatible)
Pro: No forced updates
Con: SDK complexity grows
3. API versioning in client
stripe_version = "2024-04-01" header
SDK always latest, but sends version header
Breaking: None
Pro: Decoupled versioning
Con: Different behavior for different clients
Stripe uses option 3.
Most use option 1 (simpler but breaks compatibility).
TypeScript SDK Design
// Type-safe SDK contract
interface APIResponse<T> {
data: T;
error?: APIError;
}
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
// Strongly typed methods
class SDK {
async users.create(data: CreateUserInput): Promise<User> {
// return type is User (not any)
// input type is validated
}
async users.list(): Promise<User[]> {
// returns User[], not unknown
}
}
// TypeScript catches errors at compile time
const user = await sdk.users.create({ name: "Alice" });
console.log(user.createdAt.getFullYear()); // ✅ Date methods available
const bad = await sdk.users.create({ invalidField: "x" }); // ❌ Error!
4. Runtime & Environment Deep Dive
Runtime Compatibility Matrix
┌─────────────┬────────────┬──────────────┬────────────┬──────────┐
│ Runtime │ fetch │ File System │ Crypto │ Timers │
├─────────────┼────────────┼──────────────┼────────────┼──────────┤
│ Browser │ ✅ native │ ❌ blocked │ ✅ Web API │ ✅ native│
│ Node.js │ ⚠️ v17+ │ ✅ fs module │ ✅ crypto │ ✅ native│
│ Edge │ ✅ native │ ❌ blocked │ ✅ native │ ✅ native│
│ React Native│ ✅ built-in│ ⚠️ limited │ ✅ native │ ✅ native│
│ Bun │ ✅ native │ ✅ native │ ✅ native │ ✅ native│
│ Deno │ ✅ native │ ✅ native │ ✅ native │ ✅ native│
└─────────────┴────────────┴──────────────┴────────────┴──────────┘
Universal SDK Pattern
// How to build SDK that works everywhere
// 1. Runtime detection
function getHttpClient(): HttpClient {
if (typeof fetch !== 'undefined') {
// Browser or edge runtime
return new FetchHttpClient();
}
if (typeof require !== 'undefined' && require('fs')) {
// Node.js
return new NodeHttpClient();
}
// Fallback
throw new Error('No HTTP client available for this runtime');
}
// 2. Adapter pattern
interface HttpClient {
request(config: RequestConfig): Promise<ResponseConfig>;
}
class FetchHttpClient implements HttpClient {
request(config: RequestConfig): Promise<ResponseConfig> {
return fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.body,
}).then(/* ... */);
}
}
class NodeHttpClient implements HttpClient {
request(config: RequestConfig): Promise<ResponseConfig> {
return new Promise((resolve, reject) => {
const req = http.request(config.url, {
method: config.method,
headers: config.headers,
});
req.write(config.body);
req.on('response', /* ... */);
});
}
}
// 3. SDK uses abstraction
export class SDK {
private httpClient = getHttpClient();
async request(method: string, path: string) {
return this.httpClient.request({
method,
url: this.baseUrl + path,
headers: this.headers,
});
}
}
SSR-Safe SDK Design
Next.js App Router complexity:
// Problem: Next.js Server Components run server-side
// Solution: SDK must detect and adapt
// 1. Client SDK (browser only)
'use client'; // Directive: this runs in browser
import { useEffect, useState } from 'react';
export function UserComponent() {
const [user, setUser] = useState(null);
useEffect(() => {
// This runs in browser, SDK can use browser APIs
sdk.auth.getUser().then(setUser);
}, []);
return <div>{user?.name}</div>;
}
// 2. Server SDK (server only)
// This runs on server, can't use browser APIs
import { sdk } from '@/lib/server-sdk';
export async function getUser() {
// SDK uses Node.js APIs
return await sdk.auth.getUser();
}
// 3. Universal SDK (handles both)
export async function initSDK() {
if (typeof window !== 'undefined') {
// Browser
return new BrowserSDK();
} else {
// Server
return new ServerSDK();
}
}
5. Developer Experience (DX) Engineering
DX Principles
Great DX SDKs follow these principles:
| Principle | Meaning | Example |
|---|---|---|
| Discoverable | Autocomplete works | client.users. → shows all methods |
| Predictable | Works like developers expect | create(), read(), update(), delete() |
| Forgiving | Helpful errors, not cryptic | ”Invalid email format” not “TypeError: undefined” |
| Documented | Examples for every use case | Code samples in docs, linked from autocomplete |
| Efficient | Minimal boilerplate | 3 lines to authenticate, not 30 |
| Consistent | Same patterns everywhere | All methods follow same structure |
| Debuggable | Easy to troubleshoot | Clear errors, logging hooks, request IDs |
| Composable | Works with other tools | Works with React Query, zod, etc. |
Error Design
// Bad error (what NOT to do)
throw new Error('Failed');
// Better error
throw new Error('Network error');
// Good error
throw new NetworkError('Failed to fetch /users: timeout after 5s');
// Excellent error
const error = new NetworkError(
'Failed to fetch /users: timeout after 5s',
{
code: 'TIMEOUT',
statusCode: null,
requestId: '123-456',
retryable: true,
nextRetryIn: 1000,
}
);
throw error;
// Usage
try {
await sdk.users.list();
} catch (error) {
if (error instanceof NetworkError && error.retryable) {
// Retry automatically or inform user
} else if (error instanceof ValidationError) {
// Show validation error to user
} else {
// Unknown error
console.error(error);
}
}
Documentation Architecture
SDK Documentation:
1. Getting Started (5 minutes)
- Install
- Hello World
- Common first steps
2. Core Concepts (15 minutes)
- How authentication works
- How requests are made
- Error handling patterns
- Rate limits
3. API Reference (searchable, generated)
- Every class, method, type
- Parameters with types
- Return types
- Throws exceptions
4. Guides & Examples
- Common use cases
- Step-by-step tutorials
- Best practices
- Anti-patterns
5. Troubleshooting
- Common errors & solutions
- Debug mode
- FAQ
- Support channels
6. TypeScript SDK Engineering Deep Dive
Advanced TypeScript Patterns
// Pattern 1: Conditional types for different response shapes
type GetResult<T extends 'single' | 'list'> =
T extends 'single' ? User : User[];
async function get<T extends 'single' | 'list'>(
type: T,
): Promise<GetResult<T>> {
// Implementation
}
const single = await get('single'); // User
const list = await get('list'); // User[]
// Pattern 2: Generic constraints
type ApiResponse<T extends object = {}> = {
data: T;
meta: {
requestId: string;
timestamp: number;
};
};
// Pattern 3: Mapped types for resource methods
type ResourceMethods<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: (id: string) => Promise<T[K]>;
};
// Pattern 4: Inference from schema
interface UserSchema {
id: string;
name: string;
email: string;
}
type User = InferSchema<typeof UserSchema>;
// User is now { id: string; name: string; email: string }
Runtime Validation + Types
import { z } from 'zod';
// Define schema (validates at runtime)
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// Extract type (for TypeScript)
type User = z.infer<typeof UserSchema>;
// SDK method
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// Validate at runtime, throws if invalid
return UserSchema.parse(data);
}
// Benefits:
// - TypeScript catches type errors at compile time
// - Runtime validation catches API contract changes
// - Single source of truth (schema)
7. React / Next.js / Astro SDK Integration
React Hooks SDK Pattern
// SDK provides hooks for React apps
export function useUser(id?: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!id) return;
setLoading(true);
sdk.users.get(id)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [id]);
return { user, loading, error };
}
// Usage
function UserProfile({ userId }: { userId: string }) {
const { user, loading, error } = useUser(userId);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user?.name}</div>;
}
Next.js App Router Integration
// Server Component (safe to expose secrets)
async function UserProfile({ userId }: { userId: string }) {
// SDK can use server credentials
const user = await serverSDK.users.get(userId);
return <div>{user.name}</div>;
}
// Client Component (use public SDK)
'use client';
function UpdateUserName({ userId }: { userId: string }) {
const [name, setName] = useState('');
async function handleSubmit() {
// SDK uses public API key or auth token
await clientSDK.users.update(userId, { name });
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button>Update</button>
</form>
);
}
React Query Integration
import { useQuery, useMutation } from '@tanstack/react-query';
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => sdk.users.list(),
});
}
export function useUpdateUser() {
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UserUpdate }) =>
sdk.users.update(id, data),
onSuccess: () => {
// Invalidate cache
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
// Usage
function UserList() {
const { data: users, isLoading } = useUsers();
const { mutate: updateUser } = useUpdateUser();
return (
<div>
{users?.map(user => (
<div key={user.id}>
{user.name}
<button onClick={() => updateUser({ id: user.id, data: { name: 'New Name' } })}>
Update
</button>
</div>
))}
</div>
);
}
8. SDK Distribution & Packaging
Package.json Strategy
{
"name": "@company/sdk",
"version": "2.4.1",
"description": "Official SDK for Company API",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./react": {
"types": "./dist/react.d.ts",
"import": "./dist/react.mjs",
"require": "./dist/react.cjs"
}
},
"sideEffects": false,
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
},
"files": ["dist"]
}
ESM/CJS Distribution
# Build outputs multiple formats
dist/
index.cjs # CommonJS (Node.js, older environments)
index.mjs # ES modules (modern bundlers)
index.d.ts # TypeScript types
index.d.mts # TypeScript types for ESM
react.cjs # React hooks (CommonJS)
react.mjs # React hooks (ESM)
react.d.ts
Semantic Versioning
MAJOR.MINOR.PATCH
1.2.3 = 1 (major) . 2 (minor) . 3 (patch)
Rules:
- MAJOR: Breaking changes (users must update code)
Example: SDK v1 → v2, API shape changed
- MINOR: New features, backward compatible
Example: v2.0 → v2.1, added new method
- PATCH: Bug fixes, no new features
Example: v2.1.0 → v2.1.1, fixed memory leak
Examples:
0.1.0 → 0.2.0 # Breaking (pre-release)
1.0.0 → 1.1.0 # New feature
1.1.0 → 1.1.1 # Bug fix
1.1.0 → 2.0.0 # Breaking change
Challenges:
- What's a "breaking change"? (subjective)
- How do you deprecate features?
- What if users depend on private APIs?
- How long to support old versions?
Release Workflow (with Changesets)
# Changesets workflow (automated releases)
# Step 1: Developer adds changeset
pnpm changeset
# Prompted to describe change, select version bump
# Step 2: CI creates release PR
# Combines all changesets, bumps version, updates CHANGELOG
# Step 3: Maintainer reviews & merges
# Triggers npm publish automatically
Benefits:
- Automated versioning (less human error)
- Clear changelog (from changesets)
- CI/CD integration (publish on merge)
- Multi-package monorepo support
9. SDK Observability & Reliability
Retry Strategy
// Retry pattern with exponential backoff
async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxAttempts: number;
initialDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
} = {
maxAttempts: 3,
initialDelayMs: 100,
maxDelayMs: 10000,
backoffMultiplier: 2,
}
): Promise<T> {
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === options.maxAttempts) throw error;
// Check if retryable
if (error instanceof NetworkError && error.statusCode >= 500) {
// Server error, probably transient
} else if (error instanceof RateLimitError) {
// Rate limited, should retry
} else {
// Not retryable (4xx, validation error, etc.)
throw error;
}
// Exponential backoff
const delay = Math.min(
options.initialDelayMs * Math.pow(options.backoffMultiplier, attempt - 1),
options.maxDelayMs
);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const users = await withRetry(() => sdk.users.list());
Logging & Telemetry
// SDK with pluggable logging
interface Logger {
debug(message: string, context?: object): void;
info(message: string, context?: object): void;
warn(message: string, context?: object): void;
error(message: string, context?: object): void;
}
class SDK {
private logger: Logger;
constructor(options: SDKOptions) {
this.logger = options.logger || new ConsoleLogger();
}
async request(method: string, path: string, data?: any) {
const requestId = generateRequestId();
const startTime = Date.now();
this.logger.debug('Request started', {
requestId,
method,
path,
});
try {
const response = await fetch(/* ... */);
const duration = Date.now() - startTime;
this.logger.info('Request completed', {
requestId,
method,
path,
statusCode: response.status,
durationMs: duration,
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('Request failed', {
requestId,
method,
path,
error: error.message,
durationMs: duration,
});
throw error;
}
}
}
// Usage
const sdk = new SDK({
logger: myCustomLogger, // Plug in Pino, Winston, etc.
});
10. Real-World SDK Case Studies
Case Study 1: Stripe SDK
Architecture:
- Thin abstraction over HTTP API
- Minimal magic, explicit parameters
- Strong TypeScript types
- Multi-language (20+ languages)
Key Design:
// Stripe's design philosophy: explicit
// Rather than: sdk.create('user', { name: 'Alice' })
// Stripe does: stripe.customers.create({ name: 'Alice' })
// Reasons:
// - Type safe (only valid resources)
// - Discoverable (autocomplete shows all resources)
// - Self-documenting (resource name visible)
Trade-offs:
- Pro: Simple, understandable, lightweight
- Con: Limited high-level abstractions
- Con: Manual error handling needed
Case Study 2: Firebase SDK
Architecture:
- Feature-rich, opinionated
- Handles auth, realtime, storage
- Framework-specific implementations
Key Design:
// Firebase: full-featured
export async function setupAuth() {
const auth = getAuth(app);
const user = await signInWithPopup(auth, provider);
return user;
}
// Firebase handles:
// - Browser detection
// - Token refresh
// - Offline support
// - Real-time updates
Trade-offs:
- Pro: Full solution (auth, DB, storage)
- Pro: Real-time subscriptions
- Con: Opinionated (limited customization)
- Con: Vendor lock-in
Case Study 3: Supabase SDK
Architecture:
- Thin wrapper over Postgres + HTTP
- Composable, modular design
- React hooks available
Key Design:
// Supabase: SQL-like, composable
const { data, error } = await supabase
.from('users')
.select('*')
.eq('status', 'active')
.limit(10);
// Reasons:
// - Familiar to SQL developers
// - Composable (build queries piece by piece)
// - Type-safe (schema-aware)
Trade-offs:
- Pro: Minimal abstraction (close to SQL)
- Pro: Composable (powerful queries)
- Con: Less hand-holding than Firebase
- Con: Requires SQL knowledge
11. Setup Guide
Step 1: Project Structure
# SDK monorepo structure
packages/
sdk/ # Core SDK
src/
index.ts # Entry point
client.ts # Core client class
types.ts # Type definitions
middleware/ # Middleware system
adapters/ # Runtime adapters
tests/
package.json
react/ # React hooks (optional)
src/
hooks/
providers/
package.json
cli/ # CLI (optional)
src/
commands/
package.json
docs/ # Documentation
getting-started.md
api-reference.md
examples/
Step 2: TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Step 3: Build Configuration (with Vite/tsup)
// vite.config.ts or build.config.ts
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
export default defineConfig({
plugins: [dts()],
build: {
lib: {
entry: 'src/index.ts',
name: 'SDK',
formats: ['es', 'cjs', 'umd'],
},
rollupOptions: {
external: ['react'], // Peer dependency
},
},
});
Step 4: CI/CD Release Setup
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- run: pnpm install
- run: pnpm build
- run: pnpm test
- name: Create Release PR
uses: changesets/action@v1
with:
publish: pnpm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
12. Tooling Comparison
| Tool | Purpose | Best For | Drawbacks |
|---|---|---|---|
| tsup | Minimalist bundler | Quick setup, ESM/CJS | Limited customization |
| Rollup | Flexible bundler | Complex builds, plugins | Steeper learning curve |
| Vite library mode | Modern bundler | ES modules first | CJS support not ideal |
| Changesets | Version/release | Multi-package monorepos | Needs CI setup |
| semantic-release | Automated releases | Single packages | Complex config |
| Typedoc | API docs from types | Auto-generated docs | Limited customization |
| API Extractor | Type analysis | Large projects | Steep learning curve |
| tRPC | Type-safe APIs | RPC-style APIs | Not REST APIs |
| Zod | Runtime validation | Schema validation | Bundle size |
| TanStack Query | Data fetching | React async state | React-specific |
13. Cheatsheet
SDK Architecture Checklist
Core SDK:
☐ HTTP client abstraction
☐ Authentication handling
☐ Error types (NetworkError, ValidationError, etc.)
☐ Middleware pipeline
☐ Request retries + backoff
☐ Request ID tracking
☐ Strong TypeScript types
☐ Runtime detection (browser/node/edge)
☐ Logging hooks
☐ Plugin system
API Design:
☐ Resource-based (users.create, not user_create)
☐ Consistent method names (create, read, update, delete)
☐ Fluent interfaces where appropriate
☐ Type-safe contracts
☐ Clear error messages
DX:
☐ Quick start (3 lines to hello world)
☐ Examples for every feature
☐ TypeScript autocomplete works
☐ Helpful errors (not cryptic)
☐ Good documentation
Packaging:
☐ ESM + CJS dual build
☐ Tree-shaking optimized (sideEffects: false)
☐ Semantic versioning
☐ Changelog maintained
☐ Type definitions included
☐ Published to npm
14. Real-World Engineering Mindset
Browser SDK Strategy
Question: How do you design an analytics SDK for browsers?
Considerations:
- Minimal bundle size (added to all pages)
- Must not block page load
- Must work with third-party CSP
- Must handle offline
- Must be reliable (dropped events = data loss)
Stripe Analytics SDK approach:
- Keep bundle <20KB gzipped (critical)
- Load asynchronously (non-blocking)
- Queue events offline
- Batch send (reduce requests)
- Use Web Workers if available (don’t block main thread)
SSR-Safe SDK
Question: How do you build an SDK that works with Next.js SSR?
Requirements:
- Must not crash during server render
- Must not leak secrets
- Must not use browser-only APIs on server
- Must transparently use browser APIs in browser
Pattern:
const client = typeof window !== 'undefined'
? new BrowserClient() // Browser APIs available
: new ServerClient(); // Use Node.js APIs
15. Brainstorm / Open Questions
SDK Architecture (15 questions)
- Should this SDK be runtime-specific or universal?
- What hidden complexity does runtime abstraction introduce?
- How will the API shape evolve as the platform grows?
- Should retries happen automatically or explicitly?
- What’s the right level of abstraction?
- How do you version an SDK across multiple runtimes?
- Should the SDK handle caching or leave it to user?
- How do you prevent the SDK from becoming a god object?
- What’s the minimum surface area for this SDK?
- Should SDKs enforce best practices or enable flexibility?
- How do you deprecate SDK features without breaking users?
- Should the SDK include observability or let users add it?
- What makes an SDK feel “native” to developers?
- How do you handle API versioning in the SDK?
- Should SDKs support offline-first patterns?
Runtime Compatibility (15 questions)
- Can this SDK truly work in browser AND Node.js AND edge?
- What APIs does your target runtime NOT have?
- How much polyfilling is acceptable?
- Should you ship different builds per runtime?
- What’s the cost of “universal” abstractions?
- How do you test across 5+ runtimes?
- Should SSR compatibility be first-class concern?
- How do hydration mismatches affect SDK design?
- What does “edge runtime” really mean for SDKs?
- Should Cloudflare Workers SDKs differ from browser?
- How much worker/Deno/Bun support is enough?
- Should older runtimes be supported long-term?
- How do environment variables differ across runtimes?
- Should SDKs detect runtime capabilities at runtime?
- What’s the bundle size cost of universal SDKs?
Developer Experience (15 questions)
- What makes SDK ergonomics feel “right”?
- Should SDKs favor explicit or magical?
- How much autocomplete is enough?
- Should error messages solve the problem or just report it?
- Should SDKs include logging by default?
- How do you make SDKs debuggable for users?
- Should SDKs provide examples in docs or autocomplete?
- What’s too much abstraction vs not enough?
- Should SDKs have multiple API styles?
- How do you make SDKs discoverable?
- Should SDKs include validation or trust the user?
- How do you make SDKs feel “lightweight”?
- Should SDKs include progress indicators?
- How do you guide users toward correct patterns?
- Should SDKs be self-explanatory or need tutorials?
Type Systems (15 questions)
- How much TypeScript complexity is acceptable?
- Should types match runtime validation?
- How do you handle unknown response shapes?
- Should types be strict or permissive?
- Can inference ever replace documentation?
- Should SDKs ship type definitions or generate them?
- How do you type SDKs for multiple API versions?
- Should SDKs use Zod/validation or just types?
- How do you handle API schema changes?
- Should response types be strict or lenient?
- Can types encode SDK constraints?
- Should SDKs expose internal types?
- How much generics is too much?
- Should types be optional for simpler usage?
- How do you evolve types without breaking?
Packaging & Distribution (15 questions)
- What’s the ideal bundle size?
- How many package variants is too many?
- Should SDKs ship ESM or CJS or both?
- How do you optimize for tree-shaking?
- Should optional features be lazy-loaded?
- How do you handle peer dependency conflicts?
- Should SDKs support IE11 or only modern?
- How do you manage SDK dependencies?
- What’s the right semantic versioning strategy?
- Should SDKs use lockfiles in source?
- How do you prevent dependency bloat?
- Should SDKs inline dependencies?
- What’s acceptable for SDK size growth?
- How do you version multiple SDKs together?
- Should SDKs auto-update or require explicit updates?
Observability & Operations (15 questions)
- Should retries be transparent or explicit?
- How do you debug SDK issues in production?
- Should SDKs collect telemetry?
- How do you handle rate limiting gracefully?
- Should SDKs include timeout defaults?
- Should SDKs support request tracing?
- How do you measure SDK performance impact?
- Should SDKs handle circuit breakers?
- Should SDKs log all requests?
- How do you alert on SDK errors?
- Should SDKs support request/response logging?
- How do you handle offline scenarios?
- Should SDKs expose internal metrics?
- How do you prevent SDK from causing OutOfMemory?
- Should SDKs support performance monitoring?
Platform Governance (15 questions)
- Should SDKs enforce security best practices?
- How do you prevent SDKs from becoming bloated?
- Should SDKs have governance boards?
- How do you handle breaking changes?
- Should SDKs support multiple auth schemes?
- How do you coordinate SDKs across runtimes?
- Should SDKs enforce naming conventions?
- How do you prevent SDK fragmentation?
- Should SDKs include generated code?
- How do you maintain backward compatibility long-term?
16. Practice Questions
Beginner (35 questions)
Q1. What does SDK stand for?
- Answer: Software Development Kit
Q2. True or False: An SDK is the same as an HTTP client.
- Answer: False
- Why: HTTP client only handles requests/responses. SDK includes authentication, error handling, types, and DX improvements.
Q3. Why would a company build an SDK instead of just documenting their API?
- Answer: SDKs reduce integration friction, improve DX, enable platform evolution, and increase adoption.
Q4. What’s an example of an SDK you’ve used?
- Answer: (Open-ended) Stripe SDK, Firebase SDK, AWS SDK, Supabase SDK, etc.
Q5. True or False: SDKs are only for JavaScript.
- Answer: False
- Why: Companies like Stripe have SDKs in 20+ languages.
Q6-Q35: (Additional beginner questions on SDK basics, packaging, APIs, runtimes)
Junior (35 questions)
Q36. What’s the difference between a universal SDK and runtime-specific SDKs?
- Answer: Universal SDK works everywhere (browser, Node, edge). Runtime-specific SDKs are optimized for one environment.
Q37. How would you structure an SDK to work in both browser and Node.js?
- Answer: Use runtime detection, abstract HTTP client, provide adapters for each runtime.
Q38. What problem does TypeScript solve for SDKs?
- Answer: Type safety catches errors at compile time, enables autocomplete, provides self-documenting API.
Senior (35 questions)
Q71. Design a React SDK that works with Next.js App Router SSR/SSG/ISR.
- Answer: Provide server SDK for SSR, client SDK for browser, hooks for React components, separate endpoints for each context.
Q72. Why might automatic retries in SDKs become dangerous?
- Answer: Retrying is only safe for idempotent operations. Retrying mutations can cause duplicates. Must check operation type.
Expert / Staff+ (35 questions)
Q106. Design SDK governance for a company with 50+ SDKs across languages.
- Answer: Architecture board owns principles (naming, versioning, auth), language leads own implementations, automated testing across SDKs, shared tooling/generators.
17. Personalized Recommendations
For Your Stack (React, Next.js, Astro, Vite, TypeScript)
Priority SDK Patterns:
- React Hooks Pattern — Most important for your stack
- Next.js Server/Client Split — Critical for SSR
- Vite Library Mode — For distribution
- TypeScript Advanced Types — For DX
- Composability with React Query — For data fetching
60-Day Learning Plan:
Week 1-2: SDK Fundamentals
- [ ] Understand SDK vs API client vs library
- [ ] Study Stripe/Firebase/Supabase architectures
- [ ] Learn HTTP client abstraction pattern
- [ ] Set up TypeScript SDK skeleton
Week 3-4: Core Architecture
- [ ] Implement middleware pipeline
- [ ] Add authentication handling
- [ ] Create error hierarchy
- [ ] Add retry logic with backoff
Week 5-6: React Integration
- [ ] Build React hooks layer
- [ ] Support Next.js App Router
- [ ] Integrate React Query patterns
- [ ] Handle SSR/hydration
Week 7-8: Polish & Distribution
- [ ] Write comprehensive docs
- [ ] Set up ESM/CJS build
- [ ] Create examples & playground
- [ ] Automate releases with Changesets
18. Official Documentation & Reference Links
Beginner
Intermediate
Advanced
Expert / Platform Engineering
19. Advanced Engineering Topics
SDK Ecosystem Governance
At scale (50+ SDKs), organizations need governance:
SDK Governance Model:
1. Architecture Board
- Define SDK principles (naming, versioning, auth)
- Review major SDK decisions
- Resolve conflicts between SDKs
2. Language-Specific Teams
- Own SDK for each language
- Maintain long-term
- Coordinate with others
3. Shared Tooling
- Code generators (from OpenAPI/GraphQL)
- Automated testing across runtimes
- Release automation
- Documentation generation
4. Community Feedback
- Public RFCs for major changes
- Issue tracking (public or private)
- User surveys
- Advisory board
Platform Engineering Economics
SDKs enable business models:
| Business Model | SDK Role | Example |
|---|---|---|
| SaaS | Reduces integration friction → faster adoption | Firebase, Vercel |
| API Marketplace | Increases developer attraction → more integrations | Stripe, GitHub |
| Developer Tools | Improves DX → faster adoption → network effects | TanStack, Prisma |
| Enterprise | Reduces deployment complexity → larger deals | AWS, Datadog |
Summary
Key Takeaways
- SDKs are leverage — Good SDK amplifies platform adoption
- DX is strategic — Developer experience directly impacts business metrics
- Runtime abstraction is hard — Universal SDKs require careful architecture
- TypeScript changes expectations — Users now expect type safety
- Versioning is difficult — Backward compatibility becomes burden
- Observability matters — SDK reliability affects user trust
- Documentation is part of SDK — Great SDK + bad docs = failure
- Governance scales SDKs — Multiple SDKs need coordinated governance
Next Steps
- Study 3 SDKs you use (Stripe, Firebase, Supabase)
- Design a small SDK for a service you maintain
- Learn advanced TypeScript patterns
- Build React hooks layer
- Set up automated releases with Changesets
Topics to Continue Learning
- GraphQL SDK generation
- Large-scale platform governance
- Package ecosystem sustainability
- AI-native SDK architecture
- Future JavaScript runtime trends