All posts
sdkdeveloper-experiencearchitectureapi

An SDK Is Not a Wrapper. It's a Promise.

SDK quality is contract fidelity plus ergonomic defaults - not HTTP calls with types

Most SDKs are HTTP clients with types.

The API has an endpoint. Someone wrote a function that calls that endpoint. They added type annotations. They called it an SDK. They shipped it.

Integration developers pick it up, use the happy path, and everything works. Then they hit an edge case. The SDK doesn't handle the API's error model consistently. Pagination exists in the docs but not in the SDK. Retry behavior differs between the Node and Python versions. The Node version silently swallows a rate limit error that the Python version surfaces correctly.

The developer spends two hours debugging. They conclude your SDK is unreliable. They drop it and write raw HTTP calls. You've failed at the one job of an SDK: making integration faster and more reliable than raw HTTP.

An SDK is a promise. It says: regardless of which language you're using, the semantics are the same, the errors look the same, the retry behavior is the same, and the defaults are sensible. Break that promise and developers will stop trusting the SDK altogether.

I – What SDKs Are Actually For

An SDK's job is to translate the API's contract into the developer's language in a way that feels native to that language's conventions.

Not to expose every API endpoint (that's a generated client, not an SDK).

Not to add business logic (that's an application, not an SDK).

Not to paper over API inconsistencies (fix the API; don't hide its bugs).

The SDK's deliverables:

  1. Method parity. Every API operation has a corresponding method. No undocumented methods. No duplicate methods.

  2. Error handling that matches the language's conventions. Python raises exceptions. Go returns (result, error). JavaScript/TypeScript returns promises that reject. The API's error model is translated into the language's idiomatic error handling.

  3. Sensible authentication defaults. The SDK reads credentials from the environment automatically. No boilerplate. Zero-config for the common case.

  4. Safe retry behavior. Transient failures are retried. Non-transient failures are not. The retry behavior is predictable and documentable.

  5. Type safety. Request and response types reflect the API contract. A change in the API's contract breaks compilation, not runtime behavior.

II – Method Surface

The surface of an SDK should be minimal. Only expose what the API actually supports. Do not add convenience methods that chain multiple API calls — those belong in the caller's application, not in the SDK.

For a memory API, the surface looks like:

// TypeScript example
class CtxVault {
  constructor(options: { apiKey: string; project?: string })

  memory: {
    remember(content: string, options?: RememberOptions): Promise<Memory>
    recall(query: string, options?: RecallOptions): Promise<Memory[]>
    list(options?: ListOptions): Promise<PaginatedResult<Memory>>
    get(id: string): Promise<Memory>
    delete(id: string): Promise<void>
  }

  context: {
    pack(query: string, options?: PackOptions): Promise<ContextPack>
  }
}

That's it. Eight methods. Every method maps to one API endpoint. No shortcuts, no magical helpers, no methods that call two endpoints to simulate a workflow.

The Python equivalent has the same eight methods, with the same parameters, with the same names (adjusted for Python snake_case conventions), with the same semantics. A developer who uses the Node SDK can pick up the Python SDK and recognize every method immediately.

This is method parity. It is not optional. It is the foundation of cross-language consistency.

III – Error Handling Contracts

The API returns a consistent error model. The SDK must translate that model into the language's idiomatic error handling — but the semantic content must be preserved exactly.

// TypeScript
try {
  const memory = await client.memory.get("mem_nonexistent")
} catch (error) {
  if (error instanceof CtxVaultError) {
    console.log(error.code)       // "not_found"
    console.log(error.message)    // "Memory with ID mem_nonexistent was not found."
    console.log(error.statusCode) // 404
    console.log(error.requestId)  // "req_xyz789"
  }
}
# Python
try:
    memory = client.memory.get("mem_nonexistent")
except CtxVaultError as e:
    print(e.code)        # "not_found"
    print(e.message)     # "Memory with ID mem_nonexistent was not found."
    print(e.status_code) # 404
    print(e.request_id)  # "req_xyz789"

Same fields. Same values. Different exception syntax. The developer switching from Node to Python can write error handling from memory.

The error hierarchy matters:

CtxVaultError (base)
├── AuthenticationError (401)
├── AuthorizationError (403)
├── NotFoundError (404)
├── ValidationError (422) — with .fields list of validation failures
├── RateLimitError (429) — with .retry_after seconds
└── ServerError (5xx)

Code that catches RateLimitError and sleeps for error.retry_after seconds should work identically in every language.

IV – Authentication Defaults

The zero-friction authentication experience:

// Works if CTXVAULT_API_KEY is set in the environment
const client = new CtxVault()

// Explicit API key
const client = new CtxVault({ apiKey: "cvk_live_..." })

// With project scope
const client = new CtxVault({ apiKey: "cvk_live_...", project: "proj_abc123" })

The SDK reads CTXVAULT_API_KEY and CTXVAULT_PROJECT from the environment automatically. If both are present, zero configuration is needed.

The project scope default is important. Without an explicit project, the SDK uses the API key's default project (or the first project in the key's allowed list). This must behave identically across all languages. If Node uses the first allowed project and Python uses the API key's metadata project, integrations will behave differently in different environments. Pick one behavior. Document it. Enforce it in tests.

V – Pagination and Retry

Pagination is where SDK ergonomics matter most.

The bad SDK:

// Developer must implement pagination manually
const page1 = await client.memory.list({ limit: 20 })
const page2 = await client.memory.list({ limit: 20, cursor: page1.pagination.cursor })
// etc.

The good SDK:

// Automatic iteration
for await (const memory of client.memory.list()) {
  process(memory)
}

// Or: collect all results
const allMemories = await client.memory.list().collect()

The SDK handles pagination internally. The developer expresses intent (iterate all memories) rather than mechanics (fetch page, check cursor, fetch next page).

Retry behavior for transient failures:

Retry on: 429 (with Retry-After), 500, 502, 503, 504
Do NOT retry on: 400, 401, 403, 404, 422
Max retries: 3
Backoff: exponential with jitter (100ms, 200ms, 400ms +/- 50%)

The retry policy is not configurable by default. If developers could configure it, half of them would set max retries to 10 with no backoff and accidentally DDoS your API when it has an issue. Sensible defaults that most developers don't need to change.

Make the retry policy configurable for advanced users, but document clearly that the defaults are safe and that misconfiguration can cause problems.

VI – Type Safety and Schema Drift

The SDK's types must reflect the API's contract exactly. If the API adds a new field, the types should reflect it. If the API removes a field, the types should stop including it.

The mechanism: generate types from the OpenAPI spec. Not from hand-written type definitions. Not from inferred types from test responses. From the spec, which is the canonical artifact.

# Generate TypeScript types from OpenAPI spec
openapi-typescript api/openapi.yaml --output sdk/typescript/src/types.ts

# Generate Python models
datamodel-code-generator --input api/openapi.yaml --output sdk/python/ctxvault/models.py

This generation runs in CI whenever the OpenAPI spec changes. If the spec changes break the generated types, CI fails before the change is merged.

Schema drift — the state where the API's actual behavior diverges from its spec, and therefore from the SDK's types — is caught early.

VII – What Breaks First

Language-specific behavior drift. The Node SDK automatically retries on 503. The Python SDK doesn't. A developer who tests with Node and deploys a Python integration discovers the behavior difference when 503s start failing silently. Fix: a shared test suite that runs against all SDK languages. Integration tests that simulate 503 responses and assert retry behavior.

Hidden retries causing duplicate writes. A memory.remember() call times out. The SDK retries. The first request succeeded server-side but the response was lost in transit. The retry creates a duplicate record. Fix: idempotency keys. Every write operation sends an Idempotency-Key header. The server deduplicates based on this key. Retries are safe because the same key produces the same result.

Breaking changes in minor versions. A method is renamed in a minor version bump. Existing integrations that use the old method name break on upgrade. Fix: SemVer discipline. Rename = breaking change = major version bump. The pain of a major version bump is acceptable. The pain of a minor version that breaks integrations is not.

SDK Parity Matrix

Feature Node Python Go
memory.remember()
memory.recall()
memory.list()
Auto-pagination
Error hierarchy
Retry with backoff
Env var auth
Idempotency keys

Every row in this matrix is a test that runs in CI. A missing checkmark is a release blocker.

SemVer Policy

  • Patch (x.x.N): Bug fixes. No behavior changes.
  • Minor (x.N.0): New methods, new optional parameters, new optional response fields. All backward compatible.
  • Major (N.0.0): Any breaking change. Renamed methods, removed methods, required parameter additions, response field removals.

When in doubt: bump major. The cost of breaking integrations exceeds the cost of a major version number change.

The SDK is the handshake between your API and the developers who build on it. Design it with the same care as the API itself.

0 comments

Join the conversation

Enjoyed this? Subscribe for more.

Get new essays on software architecture, AI systems, and engineering craft delivered to your inbox. No spam-ever.