Skip to content
AstroPaper
Go back

SDKs (Software Development Kits)

Updated:
Edit page

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

What SDKs Actually Are

An SDK (Software Development Kit) is a curated abstraction over an API that provides:

  1. Easy integration — Authentication, configuration, error handling built-in
  2. Type safety — Compiler-checked contracts instead of string-based API calls
  3. Developer ergonomics — Autocomplete, documentation, helpful errors
  4. Runtime abstraction — Works across browsers, Node.js, edge runtimes
  5. Operational features — Retries, rate limiting, telemetry, offline support

An SDK is NOT:

An SDK IS:

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

ComponentPurposeFocusLifecycle
SDKIntegrate with remote serviceEasy integration + DXLong-lived with service
LibraryProvide functionalityReusability + efficiencyIndependent versioning
FrameworkStructure applicationsArchitecture + patternsOpinionated, comprehensive
CLICommand-line interfaceAutomation + workflowsOften paired with SDK
API ClientCall HTTP APIsLow-level requestsStateless, 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

CompanySDK InvestmentResults
StripeMassive (SDKs in 20+ languages)Dominant payment platform, $95B valuation
FirebaseComplete (web, mobile, admin SDKs)Easy backend for startups, Google acquisition
VercelGrowing (Next.js + deployment APIs)Preferred deployment platform for React
CloudflareStrategic (workers + APIs)Developer-first, competitive moat
SupabaseCore (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:

PrincipleMeaningExample
DiscoverableAutocomplete worksclient.users. → shows all methods
PredictableWorks like developers expectcreate(), read(), update(), delete()
ForgivingHelpful errors, not cryptic”Invalid email format” not “TypeError: undefined”
DocumentedExamples for every use caseCode samples in docs, linked from autocomplete
EfficientMinimal boilerplate3 lines to authenticate, not 30
ConsistentSame patterns everywhereAll methods follow same structure
DebuggableEasy to troubleshootClear errors, logging hooks, request IDs
ComposableWorks with other toolsWorks 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:

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:

Case Study 2: Firebase SDK

Architecture:

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:

Case Study 3: Supabase SDK

Architecture:

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:


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

ToolPurposeBest ForDrawbacks
tsupMinimalist bundlerQuick setup, ESM/CJSLimited customization
RollupFlexible bundlerComplex builds, pluginsSteeper learning curve
Vite library modeModern bundlerES modules firstCJS support not ideal
ChangesetsVersion/releaseMulti-package monoreposNeeds CI setup
semantic-releaseAutomated releasesSingle packagesComplex config
TypedocAPI docs from typesAuto-generated docsLimited customization
API ExtractorType analysisLarge projectsSteep learning curve
tRPCType-safe APIsRPC-style APIsNot REST APIs
ZodRuntime validationSchema validationBundle size
TanStack QueryData fetchingReact async stateReact-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:

Stripe Analytics SDK approach:

  1. Keep bundle <20KB gzipped (critical)
  2. Load asynchronously (non-blocking)
  3. Queue events offline
  4. Batch send (reduce requests)
  5. 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:

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)

  1. Should this SDK be runtime-specific or universal?
  2. What hidden complexity does runtime abstraction introduce?
  3. How will the API shape evolve as the platform grows?
  4. Should retries happen automatically or explicitly?
  5. What’s the right level of abstraction?
  6. How do you version an SDK across multiple runtimes?
  7. Should the SDK handle caching or leave it to user?
  8. How do you prevent the SDK from becoming a god object?
  9. What’s the minimum surface area for this SDK?
  10. Should SDKs enforce best practices or enable flexibility?
  11. How do you deprecate SDK features without breaking users?
  12. Should the SDK include observability or let users add it?
  13. What makes an SDK feel “native” to developers?
  14. How do you handle API versioning in the SDK?
  15. Should SDKs support offline-first patterns?

Runtime Compatibility (15 questions)

  1. Can this SDK truly work in browser AND Node.js AND edge?
  2. What APIs does your target runtime NOT have?
  3. How much polyfilling is acceptable?
  4. Should you ship different builds per runtime?
  5. What’s the cost of “universal” abstractions?
  6. How do you test across 5+ runtimes?
  7. Should SSR compatibility be first-class concern?
  8. How do hydration mismatches affect SDK design?
  9. What does “edge runtime” really mean for SDKs?
  10. Should Cloudflare Workers SDKs differ from browser?
  11. How much worker/Deno/Bun support is enough?
  12. Should older runtimes be supported long-term?
  13. How do environment variables differ across runtimes?
  14. Should SDKs detect runtime capabilities at runtime?
  15. What’s the bundle size cost of universal SDKs?

Developer Experience (15 questions)

  1. What makes SDK ergonomics feel “right”?
  2. Should SDKs favor explicit or magical?
  3. How much autocomplete is enough?
  4. Should error messages solve the problem or just report it?
  5. Should SDKs include logging by default?
  6. How do you make SDKs debuggable for users?
  7. Should SDKs provide examples in docs or autocomplete?
  8. What’s too much abstraction vs not enough?
  9. Should SDKs have multiple API styles?
  10. How do you make SDKs discoverable?
  11. Should SDKs include validation or trust the user?
  12. How do you make SDKs feel “lightweight”?
  13. Should SDKs include progress indicators?
  14. How do you guide users toward correct patterns?
  15. Should SDKs be self-explanatory or need tutorials?

Type Systems (15 questions)

  1. How much TypeScript complexity is acceptable?
  2. Should types match runtime validation?
  3. How do you handle unknown response shapes?
  4. Should types be strict or permissive?
  5. Can inference ever replace documentation?
  6. Should SDKs ship type definitions or generate them?
  7. How do you type SDKs for multiple API versions?
  8. Should SDKs use Zod/validation or just types?
  9. How do you handle API schema changes?
  10. Should response types be strict or lenient?
  11. Can types encode SDK constraints?
  12. Should SDKs expose internal types?
  13. How much generics is too much?
  14. Should types be optional for simpler usage?
  15. How do you evolve types without breaking?

Packaging & Distribution (15 questions)

  1. What’s the ideal bundle size?
  2. How many package variants is too many?
  3. Should SDKs ship ESM or CJS or both?
  4. How do you optimize for tree-shaking?
  5. Should optional features be lazy-loaded?
  6. How do you handle peer dependency conflicts?
  7. Should SDKs support IE11 or only modern?
  8. How do you manage SDK dependencies?
  9. What’s the right semantic versioning strategy?
  10. Should SDKs use lockfiles in source?
  11. How do you prevent dependency bloat?
  12. Should SDKs inline dependencies?
  13. What’s acceptable for SDK size growth?
  14. How do you version multiple SDKs together?
  15. Should SDKs auto-update or require explicit updates?

Observability & Operations (15 questions)

  1. Should retries be transparent or explicit?
  2. How do you debug SDK issues in production?
  3. Should SDKs collect telemetry?
  4. How do you handle rate limiting gracefully?
  5. Should SDKs include timeout defaults?
  6. Should SDKs support request tracing?
  7. How do you measure SDK performance impact?
  8. Should SDKs handle circuit breakers?
  9. Should SDKs log all requests?
  10. How do you alert on SDK errors?
  11. Should SDKs support request/response logging?
  12. How do you handle offline scenarios?
  13. Should SDKs expose internal metrics?
  14. How do you prevent SDK from causing OutOfMemory?
  15. Should SDKs support performance monitoring?

Platform Governance (15 questions)

  1. Should SDKs enforce security best practices?
  2. How do you prevent SDKs from becoming bloated?
  3. Should SDKs have governance boards?
  4. How do you handle breaking changes?
  5. Should SDKs support multiple auth schemes?
  6. How do you coordinate SDKs across runtimes?
  7. Should SDKs enforce naming conventions?
  8. How do you prevent SDK fragmentation?
  9. Should SDKs include generated code?
  10. How do you maintain backward compatibility long-term?

16. Practice Questions

Beginner (35 questions)

Q1. What does SDK stand for?

Q2. True or False: An SDK is the same as an HTTP client.

Q3. Why would a company build an SDK instead of just documenting their API?

Q4. What’s an example of an SDK you’ve used?

Q5. True or False: SDKs are only for JavaScript.

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?

Q37. How would you structure an SDK to work in both browser and Node.js?

Q38. What problem does TypeScript solve for SDKs?


Senior (35 questions)

Q71. Design a React SDK that works with Next.js App Router SSR/SSG/ISR.

Q72. Why might automatic retries in SDKs become dangerous?


Expert / Staff+ (35 questions)

Q106. Design SDK governance for a company with 50+ SDKs across languages.


17. Personalized Recommendations

For Your Stack (React, Next.js, Astro, Vite, TypeScript)

Priority SDK Patterns:

  1. React Hooks Pattern — Most important for your stack
  2. Next.js Server/Client Split — Critical for SSR
  3. Vite Library Mode — For distribution
  4. TypeScript Advanced Types — For DX
  5. 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

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 ModelSDK RoleExample
SaaSReduces integration friction → faster adoptionFirebase, Vercel
API MarketplaceIncreases developer attraction → more integrationsStripe, GitHub
Developer ToolsImproves DX → faster adoption → network effectsTanStack, Prisma
EnterpriseReduces deployment complexity → larger dealsAWS, Datadog

Summary

Key Takeaways

  1. SDKs are leverage — Good SDK amplifies platform adoption
  2. DX is strategic — Developer experience directly impacts business metrics
  3. Runtime abstraction is hard — Universal SDKs require careful architecture
  4. TypeScript changes expectations — Users now expect type safety
  5. Versioning is difficult — Backward compatibility becomes burden
  6. Observability matters — SDK reliability affects user trust
  7. Documentation is part of SDK — Great SDK + bad docs = failure
  8. Governance scales SDKs — Multiple SDKs need coordinated governance

Next Steps

  1. Study 3 SDKs you use (Stripe, Firebase, Supabase)
  2. Design a small SDK for a service you maintain
  3. Learn advanced TypeScript patterns
  4. Build React hooks layer
  5. Set up automated releases with Changesets

Topics to Continue Learning


Edit page
Share this post:

Previous Post
Rolldown Code Splitting Deep Dive — Vite 8 + React
Next Post
TypeScript ULTIMATE Deep-Dive