TypeScript SDK Best Practices

Architecture and Design

1. Service Layer Pattern

Create a service layer to encapsulate Claude Code interactions:

// services/claude.service.ts
import { query, type SDKMessage, type QueryParams } from "@anthropic-ai/claude-code";
import { Logger } from "./logger";
import { CostTracker } from "./cost-tracker";
import { RateLimiter } from "./rate-limiter";
 
export interface ClaudeServiceConfig {
  maxRetries?: number;
  defaultMaxTurns?: number;
  enableCostTracking?: boolean;
  enableRateLimiting?: boolean;
}
 
export class ClaudeService {
  private logger: Logger;
  private costTracker: CostTracker;
  private rateLimiter: RateLimiter;
  
  constructor(private config: ClaudeServiceConfig = {}) {
    this.logger = new Logger('ClaudeService');
    this.costTracker = new CostTracker();
    this.rateLimiter = new RateLimiter();
  }
  
  async query(params: Omit<QueryParams, 'abortController'>): Promise<SDKMessage[]> {
    const abortController = new AbortController();
    
    try {
      // Apply rate limiting
      if (this.config.enableRateLimiting) {
        await this.rateLimiter.waitForSlot();
      }
      
      const messages: SDKMessage[] = [];
      
      for await (const message of query({
        ...params,
        abortController,
        options: {
          maxTurns: this.config.defaultMaxTurns || 3,
          ...params.options
        }
      })) {
        messages.push(message);
        
        // Track costs
        if (this.config.enableCostTracking && message.type === 'result') {
          this.costTracker.addCost(message.data.total_cost_usd || 0);
        }
      }
      
      this.logger.info(`Query completed: ${messages.length} messages`);
      return messages;
      
    } catch (error) {
      this.logger.error('Query failed:', error);
      throw error;
    }
  }
  
  getCostReport() {
    return this.costTracker.getReport();
  }
}

2. Repository Pattern for Prompts

Organize prompts in a maintainable way:

// repositories/prompts.repository.ts
export class PromptsRepository {
  private prompts = new Map<string, string>();
  
  constructor() {
    this.loadPrompts();
  }
  
  private loadPrompts() {
    // Code Review Prompts
    this.prompts.set('code-review.general', `
      Review this code for:
      - Code quality and maintainability
      - Potential bugs and edge cases
      - Performance implications
      - Security vulnerabilities
      - Best practice adherence
    `);
    
    this.prompts.set('code-review.security', `
      Perform a security-focused code review:
      - Check for injection vulnerabilities
      - Validate input sanitization
      - Review authentication/authorization
      - Check for sensitive data exposure
      - Identify potential attack vectors
    `);
    
    // Refactoring Prompts
    this.prompts.set('refactor.clean-code', `
      Refactor this code following clean code principles:
      - Extract methods for clarity
      - Improve naming conventions
      - Reduce complexity
      - Apply SOLID principles
      - Remove code duplication
    `);
  }
  
  getPrompt(key: string, variables?: Record<string, string>): string {
    const template = this.prompts.get(key);
    if (!template) {
      throw new Error(`Prompt not found: ${key}`);
    }
    
    // Simple variable replacement
    if (variables) {
      return Object.entries(variables).reduce(
        (prompt, [key, value]) => prompt.replace(`{{${key}}}`, value),
        template
      );
    }
    
    return template;
  }
  
  addPrompt(key: string, template: string): void {
    this.prompts.set(key, template);
  }
}

Error Handling Strategies

1. Comprehensive Error Handler

// utils/error-handler.ts
import { Anthropic } from "@anthropic-ai/sdk";
 
export class ErrorHandler {
  private retryDelays = [1000, 2000, 4000, 8000]; // Exponential backoff
  
  async handleWithRetry<T>(
    operation: () => Promise<T>,
    context: string
  ): Promise<T> {
    let lastError: Error | null = null;
    
    for (let attempt = 0; attempt <= this.retryDelays.length; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error as Error;
        
        if (!this.isRetryable(error)) {
          throw this.enhanceError(error, context);
        }
        
        if (attempt < this.retryDelays.length) {
          const delay = this.getRetryDelay(error, attempt);
          console.log(`Retrying ${context} after ${delay}ms (attempt ${attempt + 1})`);
          await this.sleep(delay);
        }
      }
    }
    
    throw this.enhanceError(
      lastError || new Error('Unknown error'),
      `${context} - Max retries exceeded`
    );
  }
  
  private isRetryable(error: any): boolean {
    if (error instanceof Anthropic.RateLimitError) return true;
    if (error instanceof Anthropic.APIConnectionError) return true;
    if (error instanceof Anthropic.InternalServerError) return true;
    if (error instanceof Anthropic.APIConnectionTimeoutError) return true;
    
    return false;
  }
  
  private getRetryDelay(error: any, attempt: number): number {
    // Use retry-after header if available
    if (error instanceof Anthropic.RateLimitError) {
      const retryAfter = error.headers?.['retry-after'];
      if (retryAfter) {
        return parseInt(retryAfter) * 1000;
      }
    }
    
    return this.retryDelays[attempt];
  }
  
  private enhanceError(error: any, context: string): Error {
    const enhanced = new Error(`[${context}] ${error.message}`);
    enhanced.name = error.name || 'ClaudeError';
    enhanced.stack = error.stack;
    (enhanced as any).originalError = error;
    
    return enhanced;
  }
  
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

2. Circuit Breaker Implementation

// utils/circuit-breaker.ts
export class CircuitBreaker {
  private failures = 0;
  private successCount = 0;
  private lastFailureTime = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  
  constructor(
    private failureThreshold = 5,
    private successThreshold = 3,
    private timeout = 60000
  ) {}
  
  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = 'half-open';
        this.successCount = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    
    if (this.state === 'half-open') {
      this.successCount++;
      if (this.successCount >= this.successThreshold) {
        this.state = 'closed';
        console.log('Circuit breaker is now CLOSED');
      }
    }
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    this.successCount = 0;
    
    if (this.failures >= this.failureThreshold) {
      this.state = 'open';
      console.error(`Circuit breaker is now OPEN (${this.failures} failures)`);
    }
  }
  
  getState() {
    return {
      state: this.state,
      failures: this.failures,
      lastFailureTime: this.lastFailureTime
    };
  }
}

Performance Optimization

1. Response Caching

// utils/response-cache.ts
interface CacheEntry {
  response: SDKMessage[];
  timestamp: number;
  hits: number;
}
 
export class ResponseCache {
  private cache = new Map<string, CacheEntry>();
  private maxSize = 100;
  private ttl = 3600000; // 1 hour
  
  constructor(options?: { maxSize?: number; ttl?: number }) {
    if (options?.maxSize) this.maxSize = options.maxSize;
    if (options?.ttl) this.ttl = options.ttl;
  }
  
  get(prompt: string): SDKMessage[] | null {
    const key = this.generateKey(prompt);
    const entry = this.cache.get(key);
    
    if (!entry) return null;
    
    // Check if expired
    if (Date.now() - entry.timestamp > this.ttl) {
      this.cache.delete(key);
      return null;
    }
    
    // Update hit count
    entry.hits++;
    
    // Move to end (LRU)
    this.cache.delete(key);
    this.cache.set(key, entry);
    
    return entry.response;
  }
  
  set(prompt: string, response: SDKMessage[]): void {
    const key = this.generateKey(prompt);
    
    // Enforce size limit
    if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
      // Remove least recently used
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(key, {
      response,
      timestamp: Date.now(),
      hits: 0
    });
  }
  
  private generateKey(prompt: string): string {
    // Create a hash of the prompt for consistent keys
    return require('crypto')
      .createHash('sha256')
      .update(prompt)
      .digest('hex');
  }
  
  getStats() {
    const entries = Array.from(this.cache.values());
    return {
      size: this.cache.size,
      totalHits: entries.reduce((sum, e) => sum + e.hits, 0),
      avgAge: entries.reduce((sum, e) => sum + (Date.now() - e.timestamp), 0) / entries.length
    };
  }
}

2. Batch Processing

// utils/batch-processor.ts
export class BatchProcessor {
  private queue: Array<{
    prompt: string;
    resolve: (value: SDKMessage[]) => void;
    reject: (error: any) => void;
  }> = [];
  
  private processing = false;
  
  constructor(
    private batchSize = 5,
    private batchDelay = 100
  ) {}
  
  async addToQueue(prompt: string): Promise<SDKMessage[]> {
    return new Promise((resolve, reject) => {
      this.queue.push({ prompt, resolve, reject });
      
      if (!this.processing) {
        setTimeout(() => this.processBatch(), this.batchDelay);
      }
    });
  }
  
  private async processBatch() {
    if (this.queue.length === 0) return;
    
    this.processing = true;
    
    // Take up to batchSize items
    const batch = this.queue.splice(0, this.batchSize);
    
    try {
      // Process in parallel
      const promises = batch.map(({ prompt }) => 
        this.processPrompt(prompt)
      );
      
      const results = await Promise.allSettled(promises);
      
      // Resolve/reject individual promises
      results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
          batch[index].resolve(result.value);
        } else {
          batch[index].reject(result.reason);
        }
      });
      
    } finally {
      this.processing = false;
      
      // Process next batch if queue not empty
      if (this.queue.length > 0) {
        setTimeout(() => this.processBatch(), this.batchDelay);
      }
    }
  }
  
  private async processPrompt(prompt: string): Promise<SDKMessage[]> {
    const messages: SDKMessage[] = [];
    
    for await (const message of query({
      prompt,
      abortController: new AbortController()
    })) {
      messages.push(message);
    }
    
    return messages;
  }
}

Security Best Practices

1. Input Sanitization

// security/input-sanitizer.ts
export class InputSanitizer {
  private readonly maxPromptLength = 10000;
  private readonly dangerousPatterns = [
    /\bexec\b/i,
    /\beval\b/i,
    /\b__proto__\b/,
    /\bconstructor\b.*\(/
  ];
  
  sanitizePrompt(prompt: string): string {
    // Length check
    if (prompt.length > this.maxPromptLength) {
      throw new Error(`Prompt exceeds maximum length of ${this.maxPromptLength}`);
    }
    
    // Remove potential code injection attempts
    let sanitized = prompt;
    
    // Remove script tags
    sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
    
    // Escape special characters
    sanitized = this.escapeHtml(sanitized);
    
    // Check for dangerous patterns
    for (const pattern of this.dangerousPatterns) {
      if (pattern.test(sanitized)) {
        console.warn(`Potentially dangerous pattern detected: ${pattern}`);
        // You might want to reject or further sanitize
      }
    }
    
    return sanitized;
  }
  
  private escapeHtml(text: string): string {
    const map: Record<string, string> = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#039;'
    };
    
    return text.replace(/[&<>"']/g, (m) => map[m]);
  }
  
  sanitizeFilePath(path: string): string {
    // Prevent directory traversal
    if (path.includes('..')) {
      throw new Error('Directory traversal detected');
    }
    
    // Remove null bytes
    const sanitized = path.replace(/\0/g, '');
    
    // Validate path characters
    if (!/^[\w\-./]+$/.test(sanitized)) {
      throw new Error('Invalid characters in file path');
    }
    
    return sanitized;
  }
}

2. API Key Management

// security/api-key-manager.ts
export class ApiKeyManager {
  private encryptedKey?: string;
  
  constructor() {
    this.loadKey();
  }
  
  private loadKey() {
    // Priority order for API key sources
    const sources = [
      () => process.env.ANTHROPIC_API_KEY,
      () => this.loadFromKeychain(),
      () => this.loadFromConfig()
    ];
    
    for (const source of sources) {
      const key = source();
      if (key && this.validateKey(key)) {
        this.encryptedKey = this.encrypt(key);
        return;
      }
    }
    
    throw new Error('No valid API key found');
  }
  
  private validateKey(key: string): boolean {
    // Basic validation
    return key.startsWith('sk-ant-') && key.length > 20;
  }
  
  private encrypt(key: string): string {
    // Simple obfuscation (use proper encryption in production)
    return Buffer.from(key).toString('base64');
  }
  
  private decrypt(encrypted: string): string {
    return Buffer.from(encrypted, 'base64').toString();
  }
  
  getKey(): string {
    if (!this.encryptedKey) {
      throw new Error('API key not loaded');
    }
    
    return this.decrypt(this.encryptedKey);
  }
  
  private loadFromKeychain(): string | null {
    // Platform-specific keychain access
    // Example for macOS using keytar
    try {
      // return require('keytar').getPassword('claude-code', 'api-key');
      return null; // Placeholder
    } catch {
      return null;
    }
  }
  
  private loadFromConfig(): string | null {
    // Load from secure config file
    try {
      const configPath = path.join(os.homedir(), '.claude', 'config.json');
      const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
      return config.api_key;
    } catch {
      return null;
    }
  }
}

Testing Strategies

1. Mock Claude Service

// test/mocks/claude-service.mock.ts
export class MockClaudeService {
  private responses = new Map<string, SDKMessage[]>();
  
  setMockResponse(prompt: string, response: SDKMessage[]): void {
    this.responses.set(prompt, response);
  }
  
  async query(params: QueryParams): Promise<SDKMessage[]> {
    const response = this.responses.get(params.prompt);
    if (!response) {
      throw new Error(`No mock response for prompt: ${params.prompt}`);
    }
    
    // Simulate streaming delay
    const messages: SDKMessage[] = [];
    
    for (const message of response) {
      await new Promise(resolve => setTimeout(resolve, 10));
      messages.push(message);
    }
    
    return messages;
  }
  
  createMockMessages(content: string): SDKMessage[] {
    return [
      {
        type: 'system',
        data: {
          configuration: { model: 'mock' },
          tools: [],
          capabilities: [],
          timestamp: new Date().toISOString()
        }
      },
      {
        type: 'user',
        data: {
          message: { role: 'user', content: 'test prompt' }
        }
      },
      {
        type: 'assistant',
        data: {
          message: { role: 'assistant', content }
        }
      },
      {
        type: 'result',
        data: {
          result: 'success',
          total_cost_usd: 0.001,
          total_time_ms: 100
        }
      }
    ];
  }
}

2. Integration Test Utilities

// test/utils/integration-test-helper.ts
export class IntegrationTestHelper {
  private tempDir: string;
  
  async setup() {
    // Create temp directory for test files
    this.tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-test-'));
  }
  
  async teardown() {
    // Clean up temp directory
    await fs.rm(this.tempDir, { recursive: true });
  }
  
  async createTestFile(name: string, content: string): Promise<string> {
    const filePath = path.join(this.tempDir, name);
    await fs.writeFile(filePath, content);
    return filePath;
  }
  
  async runWithTimeout<T>(
    operation: () => Promise<T>,
    timeout: number = 30000
  ): Promise<T> {
    return Promise.race([
      operation(),
      new Promise<T>((_, reject) => 
        setTimeout(() => reject(new Error('Test timeout')), timeout)
      )
    ]);
  }
  
  createTestQuery(options?: Partial<QueryParams>): QueryParams {
    return {
      prompt: 'Test prompt',
      abortController: new AbortController(),
      options: {
        maxTurns: 1,
        workingDirectory: this.tempDir,
        ...options?.options
      }
    };
  }
}

Monitoring and Observability

1. Metrics Collector

// monitoring/metrics-collector.ts
export interface QueryMetrics {
  prompt: string;
  startTime: number;
  endTime?: number;
  messageCount: number;
  cost?: number;
  error?: string;
  model?: string;
}
 
export class MetricsCollector {
  private metrics: QueryMetrics[] = [];
  
  startQuery(prompt: string, model?: string): QueryMetrics {
    const metric: QueryMetrics = {
      prompt: prompt.slice(0, 100), // Truncate for privacy
      startTime: Date.now(),
      messageCount: 0,
      model
    };
    
    this.metrics.push(metric);
    return metric;
  }
  
  updateMetric(metric: QueryMetrics, update: Partial<QueryMetrics>) {
    Object.assign(metric, update);
  }
  
  endQuery(metric: QueryMetrics, cost?: number, error?: string) {
    metric.endTime = Date.now();
    metric.cost = cost;
    metric.error = error;
  }
  
  getReport() {
    const totalQueries = this.metrics.length;
    const successfulQueries = this.metrics.filter(m => !m.error).length;
    const totalCost = this.metrics.reduce((sum, m) => sum + (m.cost || 0), 0);
    const avgResponseTime = this.metrics
      .filter(m => m.endTime)
      .reduce((sum, m) => sum + (m.endTime! - m.startTime), 0) / successfulQueries;
    
    return {
      totalQueries,
      successfulQueries,
      failureRate: (totalQueries - successfulQueries) / totalQueries,
      totalCost,
      avgResponseTime,
      queriesPerModel: this.groupByModel()
    };
  }
  
  private groupByModel() {
    return this.metrics.reduce((acc, m) => {
      const model = m.model || 'unknown';
      acc[model] = (acc[model] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);
  }
}

Streaming

Use the Fluent API for Streaming: For new projects, prefer the fluent, chainable API for streaming responses. It offers a more intuitive and powerful developer experience compared to the legacy async iterator pattern.

Production Checklist

  • Environment variable validation on startup
  • API key rotation mechanism
  • Comprehensive error logging
  • Rate limiting implementation
  • Cost tracking and alerts
  • Health check endpoint
  • Graceful shutdown handling
  • Request/response logging (with PII sanitization)
  • Performance monitoring
  • Circuit breaker for external dependencies
  • Input validation and sanitization
  • Response caching strategy
  • Timeout configuration
  • Retry logic with exponential backoff
  • Security headers and CORS configuration

Next Steps


Tags: claude-code typescript best-practices production security