Skip to content
Skillv1.0.0

exa-sdk-patterns

Apply production-ready exa-js SDK patterns with type safety, singletons, and wrappers. Use when implementing Exa integrations, refactoring SDK usage, or establishing team coding standards for Exa. Tri

by jeremylongshore(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from jeremylongshore/tons-of-skills-marketplace (skills/.curated/exa-sdk-patterns/SKILL.md). Install upstream with npx skills add jeremylongshore/tons-of-skills-marketplace --skill exa-sdk-patterns. Copyright stays with the author (MIT).

Exa SDK Patterns

Output

  • A reusable Exa client boundary with scoped configuration, sanitized inputs, policy controls, redacted telemetry, and safe error classification.
  • Unit/integration evidence that avoids real sensitive queries, result content, and credentials.

Overview

Production-ready patterns for the exa-js SDK. Covers client singletons, typed wrappers, error handling, retry logic, and response validation for real Exa API methods.

Prerequisites

  • exa-js installed and EXA_API_KEY configured
  • TypeScript project with strict mode
  • Familiarity with async/await and error handling

Instructions

Step 1: Client Singleton

// src/exa/client.ts
import Exa from "exa-js";

let instance: Exa | null = null;

export function getExa(): Exa {
  if (!instance) {
    const apiKey = process.env.EXA_API_KEY;
    if (!apiKey) {
      throw new Error("EXA_API_KEY not set. Get one at https://dashboard.exa.ai");
    }
    instance = new Exa(apiKey);
  }
  return instance;
}

Step 2: Typed Search Wrapper

// src/exa/search.ts
import Exa from "exa-js";
import { getExa } from "./client";

interface ExaSearchOptions {
  type?: "auto" | "neural" | "keyword" | "fast" | "instant" | "deep" | "deep-reasoning";
  numResults?: number;
  includeDomains?: string[];
  excludeDomains?: string[];
  startPublishedDate?: string;
  endPublishedDate?: string;
  category?: "company" | "research paper" | "news" | "tweet" | "personal site" | "financial report" | "people";
  includeText?: string[];
  excludeText?: string[];
}

interface ExaContentsOptions {
  text?: boolean | { maxCharacters?: number; includeHtmlTags?: boolean };
  highlights?: boolean | { maxCharacters?: number; query?: string };
  summary?: boolean | { query?: string };
  livecrawl?: "always" | "preferred" | "fallback" | "never";
  livecrawlTimeout?: number;
  subpages?: number;
  subpageTarget?: string | string[];
}

export async function exaSearch(query: string, opts: ExaSearchOptions = {}) {
  const exa = getExa();
  return exa.search(query, {
    type: opts.type ?? "auto",
    numResults: opts.numResults ?? 10,
    ...opts,
  });
}

export async function exaSearchWithContents(
  query: string,
  searchOpts: ExaSearchOptions = {},
  contentOpts: ExaContentsOptions = {}
) {
  const exa = getExa();
  return exa.searchAndContents(query, {
    type: searchOpts.type ?? "auto",
    numResults: searchOpts.numResults ?? 10,
    ...searchOpts,
    ...contentOpts,
  });
}

Step 3: Error Handling Wrapper

// src/exa/safe.ts
interface ExaResult<T> {
  data: T | null;
  error: ExaError | null;
}

interface ExaError {
  status: number;
  message: string;
  tag?: string;
  requestId?: string;
  retryable: boolean;
}

function classifyError(err: any): ExaError {
  const status = err.status || err.response?.status || 500;
  const retryable = status === 429 || status >= 500;
  return {
    status,
    message: err.message || "Unknown error",
    tag: err.error_tag || err.tag,
    requestId: err.requestId || err.request_id,
    retryable,
  };
}

export async function safeExaCall<T>(
  operation: () => Promise<T>
): Promise<ExaResult<T>> {
  try {
    const data = await operation();
    return { data, error: null };
  } catch (err: any) {
    const error = classifyError(err);
    console.error(`[Exa Error] ${error.status}: ${error.message}`, {
      tag: error.tag,
      requestId: error.requestId,
      retryable: error.retryable,
    });
    return { data: null, error };
  }
}

// Usage:
// const { data, error } = await safeExaCall(() =>
//   exa.searchAndContents("query", { numResults: 5, text: true })
// );

Step 4: Retry with Exponential Backoff

// src/exa/retry.ts
export async function withRetry<T>(
  operation: () => Promise<T>,
  config = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 30000 }
): Promise<T> {
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      return await operation();
    } catch (err: any) {
      const status = err.status || err.response?.status || 0;

      // Only retry on rate limits (429) and server errors (5xx)
      if (status !== 429 && (status < 500 || status >= 600)) throw err;
      if (attempt === config.maxRetries) throw err;

      const delay = Math.min(
        config.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500,
        config.maxDelayMs
      );
      console.log(`[Exa] Retry ${attempt + 1}/${config.maxRetries} in ${delay.toFixed(0)}ms`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error("Unreachable");
}

// Usage:
// const results = await withRetry(() =>
//   exa.searchAndContents("query", { numResults: 5, text: true })
// );

Step 5: Response Validation with Zod

// src/exa/validate.ts
import { z } from "zod";

const ExaResultSchema = z.object({
  url: z.string().url(),
  title: z.string().nullable(),
  score: z.number(),
  publishedDate: z.string().nullable().optional(),
  text: z.string().optional(),
  highlights: z.array(z.string()).optional(),
  summary: z.string().optional(),
});

const ExaSearchResponseSchema = z.object({
  results: z.array(ExaResultSchema),
  autopromptString: z.string().optional(),
});

export function validateSearchResponse(response: unknown) {
  return ExaSearchResponseSchema.parse(response);
}

Error Handling

Pattern Use Case Benefit
Singleton All API calls Single client instance, consistent config
Safe wrapper Non-critical searches Prevents uncaught exceptions
Retry logic Rate limits and 5xx Automatic recovery from transient failures
Zod validation Response processing Catches unexpected API response changes
Typed options IDE support Autocomplete and compile-time checks

Examples

Factory Pattern (Multi-tenant)

const clients = new Map<string, Exa>();

export function getExaForTenant(tenantId: string): Exa {
  if (!clients.has(tenantId)) {
    const apiKey = getTenantApiKey(tenantId); // from your config/vault
    clients.set(tenantId, new Exa(apiKey));
  }
  return clients.get(tenantId)!;
}

Combined: Safe + Retry + Typed

async function resilientSearch(query: string) {
  return safeExaCall(() =>
    withRetry(() =>
      exaSearchWithContents(
        query,
        { type: "neural", numResults: 5 },
        { text: { maxCharacters: 2000 }, highlights: true }
      )
    )
  );
}

Resources

Next Steps

Apply patterns in exa-core-workflow-a for real-world search usage.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/jeremylongshore-tons-of-skills-marketplace-exa-sdk-patterns/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

jeremylongshore-tons-of-skills-marketplace-exa-sdk-patterns.ocm.jsonjson
{
  "ocm": "1",
  "id": "jeremylongshore-tons-of-skills-marketplace-exa-sdk-patterns",
  "kind": "skill",
  "name": "exa-sdk-patterns",
  "description": "Apply production-ready exa-js SDK patterns with type safety, singletons, and wrappers. Use when implementing Exa integrations, refactoring SDK usage, or establishing team coding standards for Exa. Trigger with phrases like \"exa SDK patterns\", \"exa best practices\", \"exa code patterns\", \"idiomatic exa\", \"exa wrapper\".",
  "publisher": "jeremylongshore",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding"
    ],
    "tags": [
      "skill-md",
      "saas",
      "exa",
      "typescript",
      "patterns",
      "skills-sh"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Apply production-ready exa-js SDK patterns with type safety, singletons, and wrappers. Use when implementing Exa integrations, refactoring SDK usage, or establishing team coding standards for Exa. Trigger with phrases like \"exa SDK patterns\", \"exa best practices\", \"exa code patterns\", \"idiomatic exa\", \"exa wrapper\"."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "skills.sh",
      "repository": "https://github.com/jeremylongshore/tons-of-skills-marketplace",
      "path": "skills/.curated/exa-sdk-patterns/SKILL.md",
      "ref": "HEAD",
      "url": "https://github.com/jeremylongshore/tons-of-skills-marketplace/blob/HEAD/skills/.curated/exa-sdk-patterns/SKILL.md",
      "key": "jeremylongshore/tons-of-skills-marketplace/skills/.curated/exa-sdk-patterns/SKILL.md"
    },
    "compatibility": "Designed for Claude Code",
    "allowed_tools": [
      "Read,",
      "Write,",
      "Edit"
    ],
    "license": "MIT"
  },
  "instructions": "# Exa SDK Patterns\n\n## Output\n\n- A reusable Exa client boundary with scoped configuration, sanitized inputs, policy controls, redacted telemetry, and safe error classification.\n- Unit/integration evidence that avoids real sensitive queries, result content, and credentials.\n\n## Overview\n\nProduction-ready patterns for the `exa-js` SDK. Covers client singletons, typed wrappers, error handling, retry logic, and response validation for real Exa API methods.\n\n## Prerequisites\n\n- `exa-js` installed and `EXA_API_KEY` configured\n- TypeScript project with strict mode\n- Familiarity with async/await and e",
  "cost": {
    "context_tokens": 1703
  }
}

Fetch it by URL: GET /api/v1/registry/jeremylongshore-tons-of-skills-marketplace-exa-sdk-patterns/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.