D
Dev SOPKnowledge Base
Search
← All topics

RapidAPI, API Patterns, and HTTP Client Best Practices

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.

apirapidapihttptypescriptfetchrate-limitingpagination
Agent trigger phrases: RapidAPI · API integration · HTTP client · API rate limit · API pagination · API error handling · fetch wrapper · axios patterns

Overview

Patterns for integrating external APIs — from RapidAPI marketplace APIs to custom REST endpoints. Focus on type safety, resilience, and debuggability.

RapidAPI Authentication

All RapidAPI calls require two headers:

const headers = {
  "X-RapidAPI-Key": process.env.RAPIDAPI_KEY!,
  "X-RapidAPI-Host": "api-host.p.rapidapi.com",  // differs per API
};

const response = await fetch("https://api-host.p.rapidapi.com/endpoint", {
  headers,
});

Store RAPIDAPI_KEY in environment variables, never in code.

Generic HTTP Client Wrapper

// lib/api-client.ts
interface ApiClientConfig {
  baseUrl: string;
  defaultHeaders?: Record<string, string>;
  timeout?: number;
}

interface RequestOptions {
  method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  params?: Record<string, string | number | boolean>;
  body?: unknown;
  headers?: Record<string, string>;
}

async function request<T>(
  config: ApiClientConfig,
  path: string,
  options: RequestOptions = {}
): Promise<T> {
  const { method = "GET", params, body, headers = {} } = options;

  const url = new URL(path, config.baseUrl);
  if (params) {
    Object.entries(params).forEach(([k, v]) => {
      url.searchParams.set(k, String(v));
    });
  }

  const controller = new AbortController();
  const timeout = setTimeout(
    () => controller.abort(),
    config.timeout ?? 30_000
  );

  try {
    const response = await fetch(url.toString(), {
      method,
      headers: {
        "Content-Type": "application/json",
        ...config.defaultHeaders,
        ...headers,
      },
      body: body ? JSON.stringify(body) : undefined,
      signal: controller.signal,
    });

    if (!response.ok) {
      const text = await response.text().catch(() => "");
      throw new ApiError(response.status, `${method} ${path}: ${text}`);
    }

    return response.json() as Promise<T>;
  } finally {
    clearTimeout(timeout);
  }
}

class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = "ApiError";
  }
}

API Client Factory

function createApiClient(config: ApiClientConfig) {
  return {
    get: <T>(path: string, params?: Record<string, string | number | boolean>) =>
      request<T>(config, path, { params }),
    post: <T>(path: string, body: unknown) =>
      request<T>(config, path, { method: "POST", body }),
    put: <T>(path: string, body: unknown) =>
      request<T>(config, path, { method: "PUT", body }),
    delete: <T>(path: string) =>
      request<T>(config, path, { method: "DELETE" }),
  };
}

// Usage
const ghlClient = createApiClient({
  baseUrl: "https://rest.gohighlevel.com/v1",
  defaultHeaders: { Authorization: `Bearer ${process.env.GHL_API_KEY}` },
  timeout: 10_000,
});

const contacts = await ghlClient.get<{ contacts: Contact[] }>("/contacts", {
  locationId: "loc_xxx",
  limit: 100,
});

Rate Limiting (TypeScript)

class TokenBucketRateLimiter {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private readonly capacity: number,
    private readonly refillRate: number  // tokens per second
  ) {
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }

  async acquire(): Promise<void> {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
    this.lastRefill = now;

    if (this.tokens >= 1) {
      this.tokens -= 1;
      return;
    }

    const waitMs = (1 - this.tokens) / this.refillRate * 1000;
    await new Promise(resolve => setTimeout(resolve, waitMs));
    this.tokens = 0;
  }
}

const limiter = new TokenBucketRateLimiter(10, 2);  // 10 burst, 2/sec sustained

async function rateLimitedGet<T>(url: string): Promise<T> {
  await limiter.acquire();
  return fetch(url).then(r => r.json() as Promise<T>);
}

Retry with Exponential Backoff

async function withRetry<T>(
  fn: () => Promise<T>,
  maxAttempts = 3,
  baseDelayMs = 1000
): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      if (error instanceof ApiError && error.status === 429) {
        const delay = baseDelayMs * Math.pow(2, attempt);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      // Don't retry client errors
      if (error instanceof ApiError && error.status < 500) throw error;

      // Retry server errors
      if (attempt < maxAttempts - 1) {
        const delay = baseDelayMs * Math.pow(2, attempt);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  throw lastError!;
}

Pagination Helper

async function* paginate<T extends { items: unknown[]; nextCursor?: string }>(
  fetcher: (cursor?: string) => Promise<T>
): AsyncGenerator<T["items"][number]> {
  let cursor: string | undefined;

  do {
    const page = await fetcher(cursor);
    yield* page.items;
    cursor = page.nextCursor;
  } while (cursor);
}

// Usage
for await (const contact of paginate((cursor) =>
  ghlClient.get("/contacts", { cursor, limit: 100 })
)) {
  console.log(contact);
}

Zod Validation for API Responses

import { z } from "zod";

const ContactSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  firstName: z.string().optional(),
  lastName: z.string().optional(),
  phone: z.string().optional(),
  createdAt: z.string().datetime(),
});

type Contact = z.infer<typeof ContactSchema>;

async function fetchContact(id: string): Promise<Contact> {
  const raw = await ghlClient.get<unknown>(`/contacts/${id}`);
  return ContactSchema.parse(raw);  // throws ZodError if shape mismatch
}

API Key Management Pattern

// Always from environment — never hardcoded
const config = {
  rapidApiKey: process.env.RAPIDAPI_KEY ?? (() => { throw new Error("RAPIDAPI_KEY missing"); })(),
  dataforseoUser: process.env.DATAFORSEO_LOGIN ?? (() => { throw new Error("DATAFORSEO_LOGIN missing"); })(),
};

At startup, validate all required keys are present before accepting requests.