Claude
Skills
Sign in
Back

output-dev-http-client-create

Included with Lifetime
$97 forever

Create shared HTTP clients in src/shared/clients/ for Output SDK workflows. Use when integrating external APIs, creating service wrappers, or standardizing HTTP operations.

Backend & APIs

What this skill does


# Creating HTTP Clients

## Overview

This skill documents how to create shared HTTP clients for Output SDK workflows. Clients are stored in `src/shared/clients/` and shared across all workflows to ensure consistent error handling, retry logic, and API integration patterns.

## When to Use This Skill

- Integrating a new external API service
- Creating a reusable HTTP wrapper for a service
- Standardizing error handling for API calls
- Moving inline HTTP logic to a shared client

## Location Convention

HTTP clients are stored in the shared clients folder:

```
src/shared/clients/
├── gemini_client.ts     # Google Gemini API client
├── jina_client.ts       # Jina AI client
├── perplexity_client.ts # Perplexity API client
└── {service}_client.ts  # Your new client
```

**Important**: Clients are shared across ALL workflows. Do NOT create per-workflow HTTP clients.

## Other Shared Code Locations

```
src/shared/
├── clients/     # API clients (this skill)
├── utils/       # Utility functions & helpers
├── services/    # Business logic services
├── steps/       # Shared step definitions (optional)
└── evaluators/  # Shared evaluators (optional)
```

## Import Pattern in Workflows

Use relative imports from workflow files to shared clients:

```typescript
// CORRECT - Relative path from workflow steps.ts
import { GeminiImageService } from '../../shared/clients/gemini_client.js';
import { parseResumeWithJina } from '../../shared/clients/jina_client.js';

// From shared steps (if used)
import { JinaClient } from '../clients/jina_client.js';
```

## Critical Import Rules

### HTTP Client Import

```typescript
// CORRECT - Use @outputai/http wrapper
import { httpClient } from '@outputai/http';

// WRONG - Never use axios directly
import axios from 'axios';
```

### Error Types Import

```typescript
// CORRECT - Import error types from @outputai/core
import { FatalError, ValidationError } from '@outputai/core';

// WRONG - Custom error classes
class MyCustomError extends Error { ... }
```

### Credentials Import

```typescript
// CORRECT - Use @outputai/credentials for secrets
import { credentials } from '@outputai/credentials';
const apiKey = credentials.require('service.api_key');

// WRONG - Never use process.env for secrets
const apiKey = process.env.SERVICE_API_KEY;
```

## Basic Client Structure

### Simple Function-Based Client

```typescript
import { FatalError, ValidationError } from '@outputai/core';
import { httpClient } from '@outputai/http';
import { credentials } from '@outputai/credentials';

const API_KEY = credentials.require('service.api_key');
const BASE_URL = 'https://api.service.com';

const serviceClient = httpClient({
  prefixUrl: BASE_URL,
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    Accept: 'application/json'
  },
  timeout: 30000,
  retry: {
    limit: 3,
    statusCodes: [408, 429, 500, 502, 503, 504]
  }
});

/**
 * Fetch data from the service
 *
 * @param query - Search query string
 * @returns Processed response data
 * @throws {FatalError} If authentication fails or resource not found
 * @throws {ValidationError} If temporary error occurs
 */
export async function fetchServiceData(query: string): Promise<ServiceResponse> {
  const response = await serviceClient.get('endpoint', {
    searchParams: { q: query }
  });

  const data = await response.json();

  if (!data.results) {
    throw new FatalError('No results returned from service');
  }

  return data;
}
```

### Class-Based Client

```typescript
import { FatalError, ValidationError } from '@outputai/core';
import { httpClient } from '@outputai/http';

export interface ServiceOptions {
  model?: string;
  timeout?: number;
}

export class ServiceClient {
  private readonly client: ReturnType<typeof httpClient>;
  private readonly model: string;

  constructor(apiKey?: string) {
    const key = apiKey ?? credentials.require('service.api_key');

    this.client = httpClient({
      prefixUrl: 'https://api.service.com',
      headers: {
        Authorization: `Bearer ${key}`,
        'Content-Type': 'application/json'
      },
      timeout: 30000,
      retry: {
        limit: 3,
        statusCodes: [408, 429, 500, 502, 503, 504]
      }
    });

    this.model = 'default-model';
  }

  async process(input: ProcessInput): Promise<ProcessOutput> {
    try {
      const response = await this.client.post('process', {
        json: {
          model: this.model,
          input
        }
      });

      return await response.json();
    } catch (error: unknown) {
      const err = error as { status?: number; message?: string };

      if (err.status === 429) {
        throw new ValidationError(`Rate limit exceeded: ${err.message}`);
      }

      if (err.status === 401 || err.status === 403) {
        throw new FatalError(`Authentication failed: ${err.message}`);
      }

      throw new ValidationError(`Service call failed: ${err.message}`);
    }
  }
}
```

## Real-World Examples

### Example 1: Jina Client (Function-Based)

```typescript
import { FatalError } from '@outputai/core';
import { httpClient } from '@outputai/http';

const JINA_API_KEY = process.env.JINA_API_KEY || '';
const JINA_BASE_URL = 'https://r.jina.ai';

const jinaClient = httpClient({
  prefixUrl: JINA_BASE_URL,
  headers: {
    Authorization: `Bearer ${JINA_API_KEY}`,
    Accept: 'application/json'
  },
  timeout: 30000,
  retry: {
    limit: 3,
    statusCodes: [408, 413, 429, 500, 502, 503, 504]
  }
});

/**
 * Parse PDF resume using Jina Reader API
 */
export async function parseResumeWithJina(base64Pdf: string): Promise<string> {
  const response = await jinaClient.post('', {
    json: { pdf: base64Pdf },
    headers: {
      'Content-Type': 'application/json'
    }
  });

  const data: {
    data: {
      content: string;
      title?: string;
    };
  } = await response.json();

  if (!data.data?.content) {
    throw new FatalError('No content returned from Jina PDF parser');
  }

  return data.data.content;
}

/**
 * Scrape text content from URL using Jina Reader
 */
export async function scrapeTextWithJina(url: string): Promise<string> {
  const response = await jinaClient.get(url, {
    headers: {
      'X-Return-Format': 'text',
      'X-No-Cache': 'true',
      'X-Timeout': '30'
    }
  });

  const data: {
    data: {
      text?: string;
      content?: string;
    };
  } = await response.json();

  const textContent = data.data?.text || data.data?.content;

  if (!textContent) {
    throw new FatalError(`No text content returned from URL: ${url}`);
  }

  return textContent;
}
```

### Example 2: Gemini Client (Class-Based)

```typescript
import { GoogleGenerativeAI } from '@google/generative-ai';
import { FatalError, ValidationError } from '@outputai/core';

export interface GeminiImageGenerationOptions {
  prompt: string;
  referenceImages?: Array<{
    inlineData: {
      mimeType: string;
      data: string;
    };
  }>;
  aspectRatio?: string;
  resolution?: string;
  numberOfImages?: number;
}

export class GeminiImageService {
  private readonly client: GoogleGenerativeAI;
  // current as of 2026-05-04 — run output-dev-model-selection for the latest
  private readonly model: string = 'gemini-3-pro-image';

  constructor(apiKey = process.env.GOOGLE_GEMINI_API_KEY || process.env.GOOGLE_CLOUD_API_KEY) {
    if (!apiKey) {
      throw new FatalError(
        'GeminiImageService: No API Key provided (GOOGLE_GEMINI_API_KEY or GOOGLE_CLOUD_API_KEY).'
      );
    }
    this.client = new GoogleGenerativeAI(apiKey);
  }

  async generateImage(options: GeminiImageGenerationOptions): Promise<string[]> {
    const { prompt, referenceImages = [], aspectRatio = '1:1', resolution = '1K', numberOfImages = 1 } = options;

    try {
      const model = this.client.getGenerativeModel({ model: this.model });

      const parts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = [];

      if (referenceImages.length > 0) {
        referenceImages.forEach(img =>

Related in Backend & APIs