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> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
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
- Review Troubleshooting for common issues
- Explore Advanced Patterns for complex scenarios
- Practice with Workshop Exercises
Tags: claude-code typescript best-practices production security