| name | output-dev-http-client-create |
| description | Create shared HTTP clients in src/clients/ for Output SDK workflows. Use when integrating external APIs, creating service wrappers, or standardizing HTTP operations. |
| allowed-tools | Read, Write, Edit, Glob |
Creating HTTP Clients
Overview
This skill documents how to create shared HTTP clients for Output SDK workflows. Clients are stored in src/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 central clients folder:
src/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.
Import Pattern in Workflows
Use the #clients path alias to import clients:
// CORRECT - Use path alias
import { GeminiImageService } from '#clients/gemini_client.js';
import { parseResumeWithJina } from '#clients/jina_client.js';
// WRONG - Relative path from workflow
import { GeminiImageService } from '../../../clients/gemini_client.js';
Critical Import Rules
HTTP Client Import
// CORRECT - Use @output.ai/http wrapper
import { httpClient } from '@output.ai/http';
// WRONG - Never use axios directly
import axios from 'axios';
Error Types Import
// CORRECT - Import error types from @output.ai/core
import { FatalError, ValidationError } from '@output.ai/core';
// WRONG - Custom error classes
class MyCustomError extends Error { ... }
Basic Client Structure
Simple Function-Based Client
import { FatalError, ValidationError } from '@output.ai/core';
import { httpClient } from '@output.ai/http';
const API_KEY = process.env.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 '@output.ai/core';
import { httpClient } from '@output.ai/http';
export interface ServiceOptions {
model?: string;
timeout?: number;
}
export class ServiceClient {
private readonly client: ReturnType<typeof httpClient>;
private readonly model: string;
constructor(apiKey = process.env.SERVICE_API_KEY) {
if (!apiKey) {
throw new FatalError(
'ServiceClient: No API Key provided (SERVICE_API_KEY)'
);
}
this.client = httpClient({
prefixUrl: 'https://api.service.com',
headers: {
Authorization: `Bearer ${apiKey}`,
'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 '@output.ai/core';
import { httpClient } from '@output.ai/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 '@output.ai/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;
private readonly model: string = 'gemini-3-pro-image-preview';
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. Validate API Keys Early
constructor(apiKey = process.env.API_KEY) {
if (!apiKey) {
throw new FatalError('API_KEY environment variable not set');
}
// ...
}
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/clients/directory - File named
{service}_client.ts -
httpClientimported from@output.ai/http(not axios) -
FatalErrorandValidationErrorimported from@output.ai/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-folder-structure- Understanding project layoutoutput-error-http-client- Troubleshooting HTTP issuesoutput-error-try-catch- Proper error handling patterns