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