This document describes the internal architecture, design decisions, and patterns used in @luziadev/sdk.
- Zero runtime dependencies - Uses only native browser/Node.js APIs (
fetch,AbortController) - Type safety - Full TypeScript with types generated from OpenAPI spec
- Small bundle size - ~6.6 KB minified, ~2.5 KB gzipped
- Predictable error handling - Typed error classes for every failure mode
- Automatic retries - Exponential backoff with jitter for transient failures
packages/sdk/
βββ scripts/
β βββ generate-types.ts # OpenAPI β TypeScript generator
β βββ manual-test.ts # Manual testing script
βββ src/
β βββ index.ts # Public exports
β βββ client.ts # Luzia class
β βββ errors.ts # Error classes and factories
β βββ retry.ts # Retry logic with exponential backoff
β βββ types/
β β βββ index.ts # Type re-exports and aliases
β β βββ generated.ts # Auto-generated from OpenAPI
β β βββ options.ts # SDK-specific config types
β βββ resources/
β β βββ index.ts # Resource exports
β β βββ exchanges.ts # ExchangesResource
β β βββ markets.ts # MarketsResource
β β βββ tickers.ts # TickersResource
β βββ __tests__/ # Unit tests
βββ package.json
βββ tsconfig.json
βββ README.md # Usage documentation
βββ ARCHITECTURE.md # This file
Types are generated from the OpenAPI specification to ensure the SDK stays in sync with the API contract.
βββββββββββββββββββββββ
β apps/api/ β
β openapi.yaml β β Single source of truth
βββββββββββ¬ββββββββββββ
β
β bun run generate
β (openapi-typescript)
βΌ
βββββββββββββββββββββββ
β src/types/ β
β generated.ts β β Auto-generated types
βββββββββββ¬ββββββββββββ
β
β Re-export with aliases
βΌ
βββββββββββββββββββββββ
β src/types/ β
β index.ts β β Developer-friendly exports
βββββββββββββββββββββββ
- Catches breaking changes - If the API contract changes, TypeScript compilation fails
- No type drift - Types always match what the API actually returns
- Single maintenance point - Update OpenAPI spec, regenerate types
bun run generateThis reads /apps/api/openapi.yaml and outputs src/types/generated.ts.
src/types/index.ts re-exports generated types with friendly names:
// Re-export from generated (single source of truth)
export type Exchange = components['schemas']['Exchange']
export type Ticker = components['schemas']['Ticker']
// SDK-specific types (not in OpenAPI)
export type { LuziaOptions, RetryOptions } from './options.ts'The SDK uses a resource-based pattern similar to Stripe's SDK:
luzia.exchanges.list()
luzia.tickers.get('binance', 'BTC/USDT')
luzia.markets.list('binance', { quote: 'USDT' })Each resource is a class that receives the client instance:
class TickersResource {
constructor(private readonly client: Luzia) {}
async get(exchange: string, symbol: string): Promise<Ticker> {
return this.client.request(`/v1/ticker/${exchange}/${symbol}`)
}
}ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Resource β βββΆ β Client β βββΆ β Retry β
β Method β β request() β β withRetry β
ββββββββββββββββ ββββββββββββββββ ββββββββ¬ββββββββ
β
ββββββββββββββββ βββββββββΌββββββββ
β Response β βββ β _doRequest β
β or Error β β (fetch) β
ββββββββββββββββ βββββββββββββββββ
Users can inject a custom fetch implementation for testing or framework integration:
const client = new Luzia({
apiKey: 'lz_xxx',
fetch: customFetch, // For testing or React Query integration
})The SDK uses a single LuziaError class with a code property to distinguish error types:
class LuziaError extends Error {
readonly status?: number // HTTP status (e.g., 401, 404, 429)
readonly code: ErrorCode // Error category (e.g., 'auth', 'not_found')
readonly correlationId?: string
readonly retryAfter?: number // For rate_limit errors
readonly details?: Record<string, unknown> // For validation errors
readonly timeoutMs?: number // For timeout errors
}| Code | Description | HTTP Status |
|---|---|---|
auth |
Authentication failed | 401 |
not_found |
Resource not found | 404 |
validation |
Invalid request | 400 |
rate_limit |
Rate limit exceeded | 429 |
timeout |
Request timed out | - |
network |
Connection error | - |
server |
Server error | 5xx |
unknown |
Unknown error | varies |
try {
await luzia.tickers.get('binance', 'BTC/USDT')
} catch (error) {
if (error instanceof LuziaError) {
switch (error.code) {
case 'rate_limit':
console.log(`Retry after ${error.retryAfter}s`)
break
case 'auth':
console.log('Invalid API key')
break
case 'not_found':
console.log('Resource not found')
break
}
}
}// Retryable codes
- 'rate_limit' // 429
- 'network' // Connection failures
- 'timeout' // Request timeouts
- 'server' // 5xx errors
// Non-retryable codes
- 'auth' // 401
- 'validation' // 400
- 'not_found' // 404
- 'unknown' // Other errorsfunction calculateRetryDelay(attempt: number, options: RetryOptions): number {
// Base delay doubles each attempt: 1s, 2s, 4s, 8s...
let delay = options.initialDelayMs * options.backoffMultiplier ** attempt
// Add jitter (Β±50%) to prevent thundering herd
if (options.jitter) {
delay = delay * (0.5 + Math.random())
}
// Cap at maximum delay
return Math.min(delay, options.maxDelayMs)
}For 429 responses, the SDK respects the Retry-After header:
if (rateLimitError) {
delay = rateLimitError.retryAfter * 1000 + 100 // Add 100ms buffer
}The client parses rate limit headers and exposes them:
// After any request
const info = luzia.rateLimitInfo
// {
// limit: 100,
// remaining: 95,
// reset: 1704067260,
// dailyLimit: 5000, // Free tier only
// dailyRemaining: 4900, // Free tier only
// dailyReset: 1704153600 // Free tier only
// }The SDK is optimized for minimal bundle size:
| Metric | Size |
|---|---|
| Minified | ~6.6 KB |
| Gzipped | ~2.5 KB |
- Zero dependencies - No lodash, axios, etc.
- Native APIs only - fetch, AbortController, setTimeout
- Tree-shakeable exports - Import only what you need
- No polyfills - Requires Node 18+ or modern browsers
- Create
src/resources/newresource.ts:
import type { Luzia } from '../client.ts'
import type { NewType } from '../types/index.ts'
export class NewResource {
constructor(private readonly client: Luzia) {}
async get(id: string): Promise<NewType> {
return this.client.request(`/v1/new/${id}`)
}
}- Add to client in
src/client.ts:
import { NewResource } from './resources/newresource.ts'
class Luzia {
readonly newResource: NewResource
constructor(options: LuziaOptions) {
this.newResource = new NewResource(this)
}
}- Export from
src/resources/index.tsandsrc/index.ts
- Update
/apps/api/openapi.yamlwith new schema - Run
bun run generateto regenerate types - Add re-export in
src/types/index.tsif needed
bun testTests use mocked fetch to verify:
- Request URL construction
- Header injection
- Error handling
- Retry behavior
# Start API server
bun dev:api
# Run manual test
bun run scripts/manual-test.ts lz_your_api_keyNone - Zero runtime dependencies.
| Package | Purpose |
|---|---|
openapi-typescript |
Generate types from OpenAPI |
typescript |
Type checking |
@types/bun |
Bun runtime types |