• Projects
  • Service
  • About
  • branding.bz
  • Podcast
  • Tips
  • FAQ
  • Recruit
  • Download
  • Contact
  • branding.bz(ブランド構築SaaS)
  • DESIGN NOW(デザインメディア)
  • X
  • LinkedIn
  • Spotify
  • Facebook

213-0011 神奈川県川崎市高津区久本3-6-7-303

© 2026 ID INC. All rights reserved

claude-skills/スキル
SKILLOfficialdevelopment

output-dev-http-client-create

プラグイン
outputai
ソース
GitHub で見る ↗
説明

**src/shared/clients/** に共有のHTTPクライアント(外部サービスとの通信機能)を作成して、Output SDKのワークフロー全体で活用します。 次のような場合に使用: - 外部のAPI(外部サービスの機能)と連携する - サービスのラッパー(外部機能をまとめたインターフェース)を作成する - HTTP通信の処理を統一・標準化する

原文を表示

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

ユースケース
  • 外部APIと連携するとき
  • サービスのラッパーを作成するとき
  • HTTP通信を統一・標準化するとき
本文

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:

// 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

// CORRECT - Use @outputai/http wrapper
import { createKyClient } from '@outputai/http';

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

Error Types Import

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

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

Credentials Import

// 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

import { FatalError, ValidationError } from '@outputai/core';
import { createKyClient } from '@outputai/http';
import { credentials } from '@outputai/credentials';

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

const client = createKyClient({
  prefix: 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 client.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

import { FatalError, ValidationError } from '@outputai/core';
import { createKyClient } from '@outputai/http';
import { credentials } from '@outputai/credentials';

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

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

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

    this.client = createKyClient({
      prefix: '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)

import { FatalError } from '@outputai/core';
import { createKyClient } from '@outputai/http';
import { credentials } from '@outputai/credentials';

const JINA_API_KEY = credentials.require('jina.api_key');
const JINA_BASE_URL = 'https://r.jina.ai';

const client = createKyClient({
  prefix: 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 client.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 client.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)

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

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 = credentials.require('google.api_key')) {
    if (!apiKey) {
      throw new FatalError(
        'GeminiImageService: No API Key provided (google.api_key credential).'
      );
    }
    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 => parts.push(img));
      }

      const finalPrompt = `${prompt}\n\nGenerate this as a ${aspectRatio} aspect ratio image at ${resolution} resolution.`;
      parts.push({ text: finalPrompt });

      const result = await model.generateContent({
        contents: [{ role: 'user', parts }],
        generationConfig: {
          temperature: 1.0,
          topP: 0.95,
          candidateCount: numberOfImages,
          maxOutputTokens: 8192
        }
      });

      const images: string[] = [];
      const candidates = result.response.candidates || [];

      for (const candidate of candidates) {
        if (candidate.content?.parts) {
          for (const part of candidate.content.parts) {
            if (part.inlineData?.data) {
              images.push(part.inlineData.data);
            }
          }
        }
      }

      if (images.length === 0) {
        throw new ValidationError('No images were generated by Gemini');
      }

      return images;
    } catch (error: unknown) {
      const err = error as { status?: number; message?: string };

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

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

      throw new ValidationError(`Gemini image generation failed: ${err.message}`);
    }
  }
}

Error Handling Patterns

HTTP Status Code Handling

const RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
const FATAL_STATUS_CODES = [401, 403, 404];

const client = createKyClient({
  retry: {
    limit: 3,
    statusCodes: RETRY_STATUS_CODES
  },
  hooks: {
    beforeError: [
      ( { error } ) => {
        const status = error.response?.status;
        const message = error.message;

        if (status && FATAL_STATUS_CODES.includes(status)) {
          throw new FatalError(`HTTP ${status} error: ${message}`);
        }

        throw new ValidationError(`HTTP request failed: ${message}`);
      }
    ]
  }
});

Error Type Guidelines

Status Code Error Type Reason
401, 403 FatalError Auth failures won't succeed on retry
404 FatalError Resource doesn't exist
408 ValidationError Timeout, may succeed on retry
429 ValidationError Rate limit, will succeed after wait
500+ ValidationError Server errors may be temporary

Best Practices

1. Use Credentials for API Keys

Prefer @outputai/credentials over process.env for secrets management. See output-dev-credentials skill for details.

import { credentials } from '@outputai/credentials';

// credentials.require() throws MissingCredentialError if not found
const apiKey = credentials.require('service.api_key');

// credentials.get() returns undefined or default if not found
const region = credentials.get('aws.region', 'us-east-1');

2. Document Functions with JSDoc

/**
 * Fetch user profile from external service
 *
 * @param userId - Unique user identifier
 * @returns User profile data
 * @throws {FatalError} If user not found or auth fails
 * @throws {ValidationError} If temporary error occurs
 *
 * @example
 * const profile = await fetchUserProfile('user-123');
 */
export async function fetchUserProfile(userId: string): Promise<UserProfile> {
  // ...
}

3. Use Consistent Timeouts

// Standard timeout: 30 seconds
timeout: 30000

// Long-running operations: 60 seconds
timeout: 60000

4. Consume or Cancel Response Bodies

createKyClient follows Fetch response-body semantics: callers own returned response bodies. Prefer body readers like .json() or .text() when the payload is needed. If a request only reads metadata such as response.url, response.status, or headers, cancel the unused body in a finally block.

const response = await client.get( url );

try {
  return response.url;
} finally {
  await response.body?.cancel();
}

HEAD requests do not have response bodies, so this is only needed for methods that can return one.

5. Export TypeScript Interfaces

// Export interfaces for consumers
export interface ServiceResponse {
  data: {
    id: string;
    content: string;
  };
  metadata: {
    processedAt: string;
  };
}

Verification Checklist

  • [ ] Client file located in src/shared/clients/ directory
  • [ ] File named {service}_client.ts
  • [ ] createKyClient imported from @outputai/http (not axios)
  • [ ] FatalError and ValidationError imported from @outputai/core
  • [ ] API key validation in constructor/initialization
  • [ ] Retry configuration for appropriate status codes
  • [ ] FatalError used for 401, 403, 404 responses
  • [ ] ValidationError used for 429, 5xx responses
  • [ ] Non-HEAD responses are consumed or cancelled when only metadata is used
  • [ ] JSDoc documentation for exported functions
  • [ ] TypeScript interfaces exported for response types

Related Skills

  • output-dev-step-function - Using clients in step functions
  • output-dev-evaluator-function - Using clients in evaluators
  • output-dev-folder-structure - Understanding project layout
  • output-dev-credentials - Encrypted secrets management
  • output-error-http-client - Troubleshooting HTTP issues
  • output-error-try-catch - Proper error handling patterns

原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。