36

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?

THE PROMPT
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.

2 Answers

19
✔

The "parse through zod or throw ValidationError" rule is what keeps this honest when the docs lie. I go one further and make the prompt require the schema to be .strict() so an unexpected new field from the upstream is a loud failure in staging instead of a silent shape drift that bites you in prod three weeks later.

18
✔

Solid. The jitter detail matters more than people think, without it every client retries on the same schedule and you DDoS the upstream in lockstep. One addition: have it wrap the retry loop so a request that's already been aborted by the caller's AbortController doesn't get retried. I've seen generated clients happily retry a request the user already cancelled.

THE PROMPT
Add: if the caller-provided AbortSignal is already aborted, throw immediately and skip all retries. Thread the same signal through every retry attempt.

Your Answer