{"slug":"rapidapi-and-api-patterns","title":"RapidAPI, API Patterns, and HTTP Client Best Practices","tags":["api","rapidapi","http","typescript","fetch","rate-limiting","pagination"],"agent_summary":"API integration patterns — RapidAPI authentication, generic HTTP client wrapper, rate limiting, retry logic, pagination helpers, error handling conventions, and TypeScript type safety for external APIs.","trigger_phrases":["RapidAPI","API integration","HTTP client","API rate limit","API pagination","API error handling","fetch wrapper","axios patterns"],"runnable":false,"markdown":"\n## Overview\n\nPatterns for integrating external APIs — from RapidAPI marketplace APIs to custom REST endpoints. Focus on type safety, resilience, and debuggability.\n\n## RapidAPI Authentication\n\nAll RapidAPI calls require two headers:\n\n```typescript\nconst headers = {\n  \"X-RapidAPI-Key\": process.env.RAPIDAPI_KEY!,\n  \"X-RapidAPI-Host\": \"api-host.p.rapidapi.com\",  // differs per API\n};\n\nconst response = await fetch(\"https://api-host.p.rapidapi.com/endpoint\", {\n  headers,\n});\n```\n\nStore `RAPIDAPI_KEY` in environment variables, never in code.\n\n## Generic HTTP Client Wrapper\n\n```typescript\n// lib/api-client.ts\ninterface ApiClientConfig {\n  baseUrl: string;\n  defaultHeaders?: Record<string, string>;\n  timeout?: number;\n}\n\ninterface RequestOptions {\n  method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n  params?: Record<string, string | number | boolean>;\n  body?: unknown;\n  headers?: Record<string, string>;\n}\n\nasync function request<T>(\n  config: ApiClientConfig,\n  path: string,\n  options: RequestOptions = {}\n): Promise<T> {\n  const { method = \"GET\", params, body, headers = {} } = options;\n\n  const url = new URL(path, config.baseUrl);\n  if (params) {\n    Object.entries(params).forEach(([k, v]) => {\n      url.searchParams.set(k, String(v));\n    });\n  }\n\n  const controller = new AbortController();\n  const timeout = setTimeout(\n    () => controller.abort(),\n    config.timeout ?? 30_000\n  );\n\n  try {\n    const response = await fetch(url.toString(), {\n      method,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        ...config.defaultHeaders,\n        ...headers,\n      },\n      body: body ? JSON.stringify(body) : undefined,\n      signal: controller.signal,\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => \"\");\n      throw new ApiError(response.status, `${method} ${path}: ${text}`);\n    }\n\n    return response.json() as Promise<T>;\n  } finally {\n    clearTimeout(timeout);\n  }\n}\n\nclass ApiError extends Error {\n  constructor(public status: number, message: string) {\n    super(message);\n    this.name = \"ApiError\";\n  }\n}\n```\n\n## API Client Factory\n\n```typescript\nfunction createApiClient(config: ApiClientConfig) {\n  return {\n    get: <T>(path: string, params?: Record<string, string | number | boolean>) =>\n      request<T>(config, path, { params }),\n    post: <T>(path: string, body: unknown) =>\n      request<T>(config, path, { method: \"POST\", body }),\n    put: <T>(path: string, body: unknown) =>\n      request<T>(config, path, { method: \"PUT\", body }),\n    delete: <T>(path: string) =>\n      request<T>(config, path, { method: \"DELETE\" }),\n  };\n}\n\n// Usage\nconst ghlClient = createApiClient({\n  baseUrl: \"https://rest.gohighlevel.com/v1\",\n  defaultHeaders: { Authorization: `Bearer ${process.env.GHL_API_KEY}` },\n  timeout: 10_000,\n});\n\nconst contacts = await ghlClient.get<{ contacts: Contact[] }>(\"/contacts\", {\n  locationId: \"loc_xxx\",\n  limit: 100,\n});\n```\n\n## Rate Limiting (TypeScript)\n\n```typescript\nclass TokenBucketRateLimiter {\n  private tokens: number;\n  private lastRefill: number;\n\n  constructor(\n    private readonly capacity: number,\n    private readonly refillRate: number  // tokens per second\n  ) {\n    this.tokens = capacity;\n    this.lastRefill = Date.now();\n  }\n\n  async acquire(): Promise<void> {\n    const now = Date.now();\n    const elapsed = (now - this.lastRefill) / 1000;\n    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);\n    this.lastRefill = now;\n\n    if (this.tokens >= 1) {\n      this.tokens -= 1;\n      return;\n    }\n\n    const waitMs = (1 - this.tokens) / this.refillRate * 1000;\n    await new Promise(resolve => setTimeout(resolve, waitMs));\n    this.tokens = 0;\n  }\n}\n\nconst limiter = new TokenBucketRateLimiter(10, 2);  // 10 burst, 2/sec sustained\n\nasync function rateLimitedGet<T>(url: string): Promise<T> {\n  await limiter.acquire();\n  return fetch(url).then(r => r.json() as Promise<T>);\n}\n```\n\n## Retry with Exponential Backoff\n\n```typescript\nasync function withRetry<T>(\n  fn: () => Promise<T>,\n  maxAttempts = 3,\n  baseDelayMs = 1000\n): Promise<T> {\n  let lastError: Error;\n\n  for (let attempt = 0; attempt < maxAttempts; attempt++) {\n    try {\n      return await fn();\n    } catch (error) {\n      lastError = error as Error;\n\n      if (error instanceof ApiError && error.status === 429) {\n        const delay = baseDelayMs * Math.pow(2, attempt);\n        await new Promise(resolve => setTimeout(resolve, delay));\n        continue;\n      }\n\n      // Don't retry client errors\n      if (error instanceof ApiError && error.status < 500) throw error;\n\n      // Retry server errors\n      if (attempt < maxAttempts - 1) {\n        const delay = baseDelayMs * Math.pow(2, attempt);\n        await new Promise(resolve => setTimeout(resolve, delay));\n      }\n    }\n  }\n\n  throw lastError!;\n}\n```\n\n## Pagination Helper\n\n```typescript\nasync function* paginate<T extends { items: unknown[]; nextCursor?: string }>(\n  fetcher: (cursor?: string) => Promise<T>\n): AsyncGenerator<T[\"items\"][number]> {\n  let cursor: string | undefined;\n\n  do {\n    const page = await fetcher(cursor);\n    yield* page.items;\n    cursor = page.nextCursor;\n  } while (cursor);\n}\n\n// Usage\nfor await (const contact of paginate((cursor) =>\n  ghlClient.get(\"/contacts\", { cursor, limit: 100 })\n)) {\n  console.log(contact);\n}\n```\n\n## Zod Validation for API Responses\n\n```typescript\nimport { z } from \"zod\";\n\nconst ContactSchema = z.object({\n  id: z.string(),\n  email: z.string().email(),\n  firstName: z.string().optional(),\n  lastName: z.string().optional(),\n  phone: z.string().optional(),\n  createdAt: z.string().datetime(),\n});\n\ntype Contact = z.infer<typeof ContactSchema>;\n\nasync function fetchContact(id: string): Promise<Contact> {\n  const raw = await ghlClient.get<unknown>(`/contacts/${id}`);\n  return ContactSchema.parse(raw);  // throws ZodError if shape mismatch\n}\n```\n\n## API Key Management Pattern\n\n```typescript\n// Always from environment — never hardcoded\nconst config = {\n  rapidApiKey: process.env.RAPIDAPI_KEY ?? (() => { throw new Error(\"RAPIDAPI_KEY missing\"); })(),\n  dataforseoUser: process.env.DATAFORSEO_LOGIN ?? (() => { throw new Error(\"DATAFORSEO_LOGIN missing\"); })(),\n};\n```\n\nAt startup, validate all required keys are present before accepting requests.\n","html":"<h2>Overview</h2>\n<p>Patterns for integrating external APIs — from RapidAPI marketplace APIs to custom REST endpoints. Focus on type safety, resilience, and debuggability.</p>\n<h2>RapidAPI Authentication</h2>\n<p>All RapidAPI calls require two headers:</p>\n<pre><code class=\"language-typescript\">const headers = {\n  \"X-RapidAPI-Key\": process.env.RAPIDAPI_KEY!,\n  \"X-RapidAPI-Host\": \"api-host.p.rapidapi.com\",  // differs per API\n};\n\nconst response = await fetch(\"https://api-host.p.rapidapi.com/endpoint\", {\n  headers,\n});\n</code></pre>\n<p>Store <code>RAPIDAPI_KEY</code> in environment variables, never in code.</p>\n<h2>Generic HTTP Client Wrapper</h2>\n<pre><code class=\"language-typescript\">// lib/api-client.ts\ninterface ApiClientConfig {\n  baseUrl: string;\n  defaultHeaders?: Record&#x3C;string, string>;\n  timeout?: number;\n}\n\ninterface RequestOptions {\n  method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n  params?: Record&#x3C;string, string | number | boolean>;\n  body?: unknown;\n  headers?: Record&#x3C;string, string>;\n}\n\nasync function request&#x3C;T>(\n  config: ApiClientConfig,\n  path: string,\n  options: RequestOptions = {}\n): Promise&#x3C;T> {\n  const { method = \"GET\", params, body, headers = {} } = options;\n\n  const url = new URL(path, config.baseUrl);\n  if (params) {\n    Object.entries(params).forEach(([k, v]) => {\n      url.searchParams.set(k, String(v));\n    });\n  }\n\n  const controller = new AbortController();\n  const timeout = setTimeout(\n    () => controller.abort(),\n    config.timeout ?? 30_000\n  );\n\n  try {\n    const response = await fetch(url.toString(), {\n      method,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        ...config.defaultHeaders,\n        ...headers,\n      },\n      body: body ? JSON.stringify(body) : undefined,\n      signal: controller.signal,\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => \"\");\n      throw new ApiError(response.status, `${method} ${path}: ${text}`);\n    }\n\n    return response.json() as Promise&#x3C;T>;\n  } finally {\n    clearTimeout(timeout);\n  }\n}\n\nclass ApiError extends Error {\n  constructor(public status: number, message: string) {\n    super(message);\n    this.name = \"ApiError\";\n  }\n}\n</code></pre>\n<h2>API Client Factory</h2>\n<pre><code class=\"language-typescript\">function createApiClient(config: ApiClientConfig) {\n  return {\n    get: &#x3C;T>(path: string, params?: Record&#x3C;string, string | number | boolean>) =>\n      request&#x3C;T>(config, path, { params }),\n    post: &#x3C;T>(path: string, body: unknown) =>\n      request&#x3C;T>(config, path, { method: \"POST\", body }),\n    put: &#x3C;T>(path: string, body: unknown) =>\n      request&#x3C;T>(config, path, { method: \"PUT\", body }),\n    delete: &#x3C;T>(path: string) =>\n      request&#x3C;T>(config, path, { method: \"DELETE\" }),\n  };\n}\n\n// Usage\nconst ghlClient = createApiClient({\n  baseUrl: \"https://rest.gohighlevel.com/v1\",\n  defaultHeaders: { Authorization: `Bearer ${process.env.GHL_API_KEY}` },\n  timeout: 10_000,\n});\n\nconst contacts = await ghlClient.get&#x3C;{ contacts: Contact[] }>(\"/contacts\", {\n  locationId: \"loc_xxx\",\n  limit: 100,\n});\n</code></pre>\n<h2>Rate Limiting (TypeScript)</h2>\n<pre><code class=\"language-typescript\">class TokenBucketRateLimiter {\n  private tokens: number;\n  private lastRefill: number;\n\n  constructor(\n    private readonly capacity: number,\n    private readonly refillRate: number  // tokens per second\n  ) {\n    this.tokens = capacity;\n    this.lastRefill = Date.now();\n  }\n\n  async acquire(): Promise&#x3C;void> {\n    const now = Date.now();\n    const elapsed = (now - this.lastRefill) / 1000;\n    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);\n    this.lastRefill = now;\n\n    if (this.tokens >= 1) {\n      this.tokens -= 1;\n      return;\n    }\n\n    const waitMs = (1 - this.tokens) / this.refillRate * 1000;\n    await new Promise(resolve => setTimeout(resolve, waitMs));\n    this.tokens = 0;\n  }\n}\n\nconst limiter = new TokenBucketRateLimiter(10, 2);  // 10 burst, 2/sec sustained\n\nasync function rateLimitedGet&#x3C;T>(url: string): Promise&#x3C;T> {\n  await limiter.acquire();\n  return fetch(url).then(r => r.json() as Promise&#x3C;T>);\n}\n</code></pre>\n<h2>Retry with Exponential Backoff</h2>\n<pre><code class=\"language-typescript\">async function withRetry&#x3C;T>(\n  fn: () => Promise&#x3C;T>,\n  maxAttempts = 3,\n  baseDelayMs = 1000\n): Promise&#x3C;T> {\n  let lastError: Error;\n\n  for (let attempt = 0; attempt &#x3C; maxAttempts; attempt++) {\n    try {\n      return await fn();\n    } catch (error) {\n      lastError = error as Error;\n\n      if (error instanceof ApiError &#x26;&#x26; error.status === 429) {\n        const delay = baseDelayMs * Math.pow(2, attempt);\n        await new Promise(resolve => setTimeout(resolve, delay));\n        continue;\n      }\n\n      // Don't retry client errors\n      if (error instanceof ApiError &#x26;&#x26; error.status &#x3C; 500) throw error;\n\n      // Retry server errors\n      if (attempt &#x3C; maxAttempts - 1) {\n        const delay = baseDelayMs * Math.pow(2, attempt);\n        await new Promise(resolve => setTimeout(resolve, delay));\n      }\n    }\n  }\n\n  throw lastError!;\n}\n</code></pre>\n<h2>Pagination Helper</h2>\n<pre><code class=\"language-typescript\">async function* paginate&#x3C;T extends { items: unknown[]; nextCursor?: string }>(\n  fetcher: (cursor?: string) => Promise&#x3C;T>\n): AsyncGenerator&#x3C;T[\"items\"][number]> {\n  let cursor: string | undefined;\n\n  do {\n    const page = await fetcher(cursor);\n    yield* page.items;\n    cursor = page.nextCursor;\n  } while (cursor);\n}\n\n// Usage\nfor await (const contact of paginate((cursor) =>\n  ghlClient.get(\"/contacts\", { cursor, limit: 100 })\n)) {\n  console.log(contact);\n}\n</code></pre>\n<h2>Zod Validation for API Responses</h2>\n<pre><code class=\"language-typescript\">import { z } from \"zod\";\n\nconst ContactSchema = z.object({\n  id: z.string(),\n  email: z.string().email(),\n  firstName: z.string().optional(),\n  lastName: z.string().optional(),\n  phone: z.string().optional(),\n  createdAt: z.string().datetime(),\n});\n\ntype Contact = z.infer&#x3C;typeof ContactSchema>;\n\nasync function fetchContact(id: string): Promise&#x3C;Contact> {\n  const raw = await ghlClient.get&#x3C;unknown>(`/contacts/${id}`);\n  return ContactSchema.parse(raw);  // throws ZodError if shape mismatch\n}\n</code></pre>\n<h2>API Key Management Pattern</h2>\n<pre><code class=\"language-typescript\">// Always from environment — never hardcoded\nconst config = {\n  rapidApiKey: process.env.RAPIDAPI_KEY ?? (() => { throw new Error(\"RAPIDAPI_KEY missing\"); })(),\n  dataforseoUser: process.env.DATAFORSEO_LOGIN ?? (() => { throw new Error(\"DATAFORSEO_LOGIN missing\"); })(),\n};\n</code></pre>\n<p>At startup, validate all required keys are present before accepting requests.</p>\n"}