🔌output-dev-http-client-create
- プラグイン
- outputai
- ソース
- GitHub で見る ↗
説明
外部向けSDKワークフロー用の共有HTTPクライアントを `src/shared/clients/` に作成します。 次のような場合に使用: 外部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 { httpClient } 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 { 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
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)
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)
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 => 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 = httpClient({
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. 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 - [ ]
httpClientimported from@outputai/http(not axios) - [ ]
FatalErrorandValidationErrorimported 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
- [ ] JSDoc documentation for exported functions
- [ ] TypeScript interfaces exported for response types
Related Skills
output-dev-step-function- Using clients in step functionsoutput-dev-evaluator-function- Using clients in evaluatorsoutput-dev-folder-structure- Understanding project layoutoutput-dev-credentials- Encrypted secrets managementoutput-error-http-client- Troubleshooting HTTP issuesoutput-error-try-catch- Proper error handling patterns
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。