Every "integrate the Stripe/Notion/whatever API" answer I got was a bare fetch with no timeout, no retry, and a catch (e) { console.log(e) } that swallowed everything. Then it fell over the first time the upstream returned a 429.
I rewrote the ask to demand a small typed client with a single request function, typed error classes, and an explicit retry policy with jitter. The generated code was boring in the best way and survived a real rate-limit storm.
How do you all keep the response types honest when the upstream docs and reality disagree?
Build a typed API client module in TypeScript for {SERVICE}. No framework, standard fetch only, but production-grade.
REQUIREMENTS:
1. A single `request<T>(path, opts)` function all endpoints go through. Every endpoint wrapper calls it; no ad-hoc fetch anywhere else.
2. Timeouts: every request uses AbortController with a default 10s timeout, overridable per call.
3. Retries: retry only on 429 and 5xx, max 3 attempts, exponential backoff with full jitter, and respect a `Retry-After` header when present. Never retry non-idempotent calls unless an explicit `idempotencyKey` is passed.
4. Errors: define `ApiError` (base), `RateLimitError`, `AuthError`, `NotFoundError`, `ValidationError`. Map HTTP status -> error class. Never throw a raw string, never swallow an error.
5. Types: model request and response bodies as explicit interfaces. Parse responses through a validator (zod) and throw ValidationError if the payload doesn't match, so the types can't silently lie.
6. No `any`, no `console.log` for errors (return/throw typed errors instead).
Deliver client.ts, errors.ts, and schemas.ts, plus a 6-line usage example that shows catching RateLimitError specifically.