# Trembita Architecture & Design A visual guide to how trembita works and how to integrate it. ## System Architecture ````text ┌─────────────────────────────────────────────────────────────────┐ │ Your Application │ └────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ createTrembita() │ │ ✓ Validates endpoint (URL) │ │ ✓ Returns Result │ └────────────────────┬────────────────────────────────────────────┘ │ ┌────────────┴────────────┐ ▼ ▼ ✅ Ok: client ❌ Error (ready to use) (handle & throw) │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ client.request(opts) │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ 1. Validate options (path/url, headers, body) ││ │ │ 2. Build URL (endpoint + path + query params) ││ │ │ 3. Serialize body (JSON) ││ │ │ 4. Apply headers + tracing headers (optional) ││ │ │ 5. Check circuit breaker status ││ │ │ 6. Call fetchImpl with Request ││ │ │ ├─ Default: globalThis.fetch ││ │ │ ├─ Custom: createRetryingFetch (retries with backoff) ││ │ │ └─ Test: vi.fn() or mock ││ │ │ 7. Handle response ││ │ │ ├─ Parse JSON ││ │ │ ├─ Check status code ││ │ │ └─ Return raw { statusCode, body } or ❌ error ││ │ └─────────────────────────────────────────────────────────────┘│ │ │ │ Returns: Promise> │ └─────────────────────────────────────────────────────────────────┘ │ ┌────────────┴────────────┐ ▼ ▼ ✅ Ok: parsed ❌ Error: one of JSON value - fetch_failed - timeout - invalid_json - unexpected_status - circuit_open - (others) │ └─────────────────────────────► Application handles both paths ```text --- ## Request Pipeline ### From Request to Response ```text Request Options │ ├─ path: '/users/123' ├─ query: { limit: 10 } ├─ headers: { Authorization: '...' } ├─ body: { name: 'Alice' } ├─ expectedCodes: [200, 201] └─ timeoutMs: 5000 │ ▼ Validation Layer ├─ path or url? (one required) ├─ headers are Record? ├─ body is serializable to JSON? └─ expectedCodes is number[]? │ ▼ URL Building ├─ Start: endpoint ('https://api.example.com') ├─ Add path ('/users/123') ├─ Append query ('?limit=10') └─ Result: 'https://api.example.com/users/123?limit=10' │ ▼ Headers Setup ├─ Merge provided headers ├─ Add Content-Type: application/json (if body) ├─ Add tracing headers (W3C traceparent, if enabled) ├─ Redact sensitive headers in logs └─ Result: final headers object │ ▼ Circuit Breaker Check ├─ Too many failures? │ └─ YES: reject with 'circuit_open' error └─ NO: continue │ ▼ Fetch (with optional retry) ├─ Call fetchImpl (default: globalThis.fetch) │ │ │ └─ If createRetryingFetch: │ ├─ Retry on 5xx? YES │ ├─ Retry on network error? YES │ ├─ Exponential backoff (100ms → 200ms → 400ms...) │ └─ Stop after maxAttempts │ ├─ Timeout? NO ──────────────┐ │ │ │ Network error? NO ──────────┤ │ │ ▼ │ Response Received │ ├─ Status code? │ ├─ Body (raw) │ │ │ ├─ Parse JSON? │ │ ├─ NO: invalid_json ──────────┐ │ └─ YES: continue │ │ │ ├─ Status in expectedCodes? │ │ ├─ YES: return { ok: true, value: body } │ └─ NO: unexpected_status ──────────┐ │ │ └──────────────────────────────────────┼─────────────┐ │ │ Return Result ▼ ▼ ┌─────────────────────────────────────────┐ │ { ok: false, error: { kind, ... } } │ └─────────────────────────────────────────┘ ```` --- ## Error Handling Flow Every operation returns `Result`: ``` Promise> │ ├─ fetch_failed │ └─ cause: Error (DNS, connection reset, etc.) │ ├─ timeout │ └─ timeoutMs: number │ ├─ circuit_open │ └─ retryAfterMs: number │ ├─ invalid_json │ └─ cause: Error (JSON.parse failed) │ ├─ unexpected_status │ ├─ statusCode: number │ ├─ body: unknown │ └─ request.expectedCodes: number[] │ └─ invalid_request_options └─ (path missing, etc.) ``` **Example error narrowing:** ```typescript const result = await client.request({ path: '/' }); if (result.ok) { // result.value is the JSON console.log(result.value); } else { // result.error is the failure switch (result.error.kind) { case 'fetch_failed': console.error('Network:', result.error.cause); break; case 'unexpected_status': console.error('HTTP', result.error.statusCode); break; default: console.error('Other:', result.error.kind); } } ``` --- ## Feature Matrix | Feature | Default | Optional | Notes | | ------------------------ | ------------------ | ----------------------------- | -------------------------------------------------- | | **Fetch implementation** | `globalThis.fetch` | Custom `fetchImpl` | Inject for testing or retries | | **Retries** | None | `createRetryingFetch()` | Exponential backoff on 5xx | | **Circuit breaker** | Disabled | `circuitBreaker: {...}` | Fail fast after N failures | | **Timeout** | Infinite | `timeoutMs: 5000` | Per-request override possible | | **Logging** | Silent | `log: {...}` | trace/debug/info/warn/error | | **Tracing** | None | `traceContextHeaders()` | W3C traceparent injection | | **Validation** | JSON parsing only | `requestWithStandardSchema()` | Standard Schema validators like Zod, Valibot, etc. | --- ## Integration Patterns ### Pattern 1: Simple REST Client ````text createTrembita() │ └─ client.request() ──► Result │ └─ if (!result.ok) { /* handle */ } ```text ### Pattern 2: With Retries ```text createRetryingFetch() ──┐ │ createTrembita()◄───────┘ │ └─ client.request() ──► Result (auto-retried on 5xx) ```text ### Pattern 3: With Circuit Breaker ```text createTrembita({ circuitBreaker: {...} }) │ └─ client.request() ──► Result │ └─ error.kind === 'circuit_open' (fail fast after N failures) ```text ### Pattern 4: Full Stack ```text createRetryingFetch() │ ├─ Retries on 5xx │ createTrembita({ fetchImpl: retrying, circuitBreaker: {...}, ──► Fast fail after N failures timeoutMs: 10000, ──► Timeout protection log: {...} ──► Structured logging }) │ └─ client.request() ──► Production-ready ```` ### Pattern 5: Testing ``` vi.fn(() => Response) │ └─ fetchImpl │ createTrembita() │ └─ client.request() ──► No network, fully controlled ``` --- ## Data Flow: From User Input to HTTP ``` User Code │ ├─ createTrembita({ endpoint: 'https://...' }) │ └─ Validates endpoint │ ├─ client.request({ │ │ path: '/users/123', │ │ query: { limit: 10 }, │ │ headers: { Authorization: '...' }, │ │ body: { name: 'Alice' } │ │}) │ │ │ ├─ Validates all options │ ├─ Builds URL │ ├─ Serializes body to JSON │ ├─ Merges headers │ │ │ ├─ Check circuit breaker │ ├─ Call fetchImpl (with optional retries) │ │ │ ├─ Parse response JSON │ ├─ Check status code │ │ │ └─ Return Result │ └─ Handle result ├─ if (result.ok) { /* success */ } └─ else { /* handle error.kind */ } ``` --- ## Module Structure ``` src/ ├─ index.ts ◄─ Public exports │ ├─ trembita.ts ◄─ createTrembita, client, request │ ├─ result.ts ◄─ Result type + helpers │ ├─ errors.ts ◄─ Error discriminated unions (types only) │ ├─ validate.ts ◄─ Options validation, Logger type │ ├─ retryingFetch.ts ◄─ createRetryingFetch │ ├─ standardSchema.ts ◄─ validateStandardSchema │ ├─ requestWithStandardSchema.ts ◄─ Validation + fetch combined │ ├─ tracing.ts ◄─ traceContextHeaders (W3C) │ └─ url.ts ◄─ URL building helpers ``` --- ## Design Principles ### 1. **Functional API** - Factory functions (`createTrembita`) not classes - Pure helpers for validation and building - No method chaining ### 2. **Result Everywhere** - No exceptions for operational errors - Errors are data (discriminated unions) - Callers must handle both paths ### 3. **Zero Dependencies** - Only `fetch` and `URL` from platform - All validation inline - No version conflicts ### 4. **Testability** - Inject `fetchImpl` to swap fetch - No global state to mock - Deterministic and composable ### 5. **Type Safety** - `strict` TypeScript - No `any` - `unknown` at JSON boundaries --- ## When to Use Each Feature | Feature | When | Example | | ------------------- | ----------------------- | ---------------------------------- | | **Basic request** | Simple API calls | `client.request({ path: '/' })` | | **Retries** | Flaky or overloaded API | Payment processors, search engines | | **Circuit breaker** | Dependent services | Microservice-to-microservice | | **Timeout** | Unresponsive upstream | External APIs, batch jobs | | **Logging** | Debugging or ops | Production logging for tracing | | **Tracing** | Distributed systems | OpenTelemetry integration | | **Validation** | Untrusted responses | Third-party APIs | --- ## Example: Full Lifecycle ```typescript // 1. Initialize (once) const api = createTrembita({ endpoint: 'https://api.stripe.com/v1', fetchImpl: createRetryingFetch({ maxAttempts: 3 }), circuitBreaker: { failureThreshold: 5, cooldownMs: 60_000 }, timeoutMs: 10_000, log: console }); if (!api.ok) throw new Error('Stripe config failed'); const client = api.value; // 2. Request (many times) const result = await client.request({ path: '/charges', method: 'POST', headers: { Authorization: `Bearer ${STRIPE_KEY}` }, body: { amount: 2000, currency: 'usd' }, expectedCodes: [200] }); // 3. Handle result if (result.ok) { console.log('Charge created:', result.value); } else { console.error('Charge failed:', result.error.kind); // Retries already tried // Circuit breaker already checked // Timeout already enforced } ``` Every part is explicit and testable.