Fluent API Documentation
June 25, 2025 ยท View on GitHub
The Claude Code SDK now includes a powerful fluent API that makes it easier to build and execute queries with a chainable interface.
Table of Contents
- Getting Started
- Query Builder
- Response Parser
- Session Management
- Logging Framework
- Advanced Patterns
- Migration Guide
Getting Started
The fluent API provides a more intuitive way to interact with Claude Code:
import { claude } from '@instantlyeasy/claude-code-sdk-ts';
// Simple example
const response = await claude()
.withModel('sonnet')
.skipPermissions()
.query('Hello, Claude!')
.asText();
Query Builder
The QueryBuilder class provides chainable methods for configuring your query:
Model Configuration
claude()
.withModel('opus') // or 'sonnet'
.withTimeout(60000) // 60 seconds
.debug(true) // Enable debug mode
Tool Management
claude()
.allowTools('Read', 'Write', 'Edit') // Explicitly allow tools
.denyTools('Bash', 'WebSearch') // Explicitly deny tools
.allowTools() // No arguments = read-only mode (denies all tools)
Permissions
claude()
.skipPermissions() // Bypass all permission prompts
.acceptEdits() // Auto-accept file edits
.withPermissions('default') // Use default permission handling
Environment Configuration
claude()
.inDirectory('/path/to/project')
.withEnv({ NODE_ENV: 'production' })
Directory Context
claude()
.addDirectory('/path/to/dir') // Add single directory
.addDirectory(['../apps', '../lib']) // Add multiple directories
.addDirectory('/another/dir') // Accumulate with multiple calls
The addDirectory method allows you to add additional working directories for Claude to access:
- Single directory: Pass a string path
- Multiple directories: Pass an array of string paths
- Accumulative: Multiple calls to
addDirectorywill accumulate all directories - CLI mapping: Generates
--add-dirflag with space-separated paths
MCP Servers
claude()
.withMCP(
{ command: 'mcp-server-filesystem', args: ['--readonly'] },
{ command: 'mcp-server-git' }
)
Event Handlers
claude()
.onMessage(msg => console.log('Message:', msg.type))
.onAssistant(content => console.log('Assistant says...'))
.onToolUse(tool => console.log(`Using ${tool.name}`))
Response Parser
The ResponseParser provides multiple ways to extract data from Claude's responses:
Basic Parsing Methods
const parser = claude().query('Your prompt');
// Get raw text
const text = await parser.asText();
// Get final result
const result = await parser.asResult();
// Get all messages
const messages = await parser.asArray();
Structured Data Extraction
// Extract JSON from response
const data = await parser.asJSON<MyInterface>();
// Get tool execution details
const executions = await parser.asToolExecutions();
// Find specific tool results
const fileContents = await parser.findToolResults('Read');
const firstFile = await parser.findToolResult('Read');
Usage Statistics
const usage = await parser.getUsage();
console.log(`Tokens: ${usage.totalTokens}`);
console.log(`Cost: $${usage.totalCost}`);
Streaming
Important Note: This SDK streams complete messages, not individual tokens. Each assistant message contains the full text block, not incremental updates.
await parser.stream(async (message) => {
if (message.type === 'assistant') {
// Each message contains complete text, not token-by-token updates
console.log(message.content[0].text); // Full text block
}
});
Cancellation with AbortSignal
import { claude, AbortError } from '@instantlyeasy/claude-code-sdk-ts';
const controller = new AbortController();
const parser = claude()
.withSignal(controller.signal)
.query('Long running query');
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
await parser.stream(async (message) => {
// Process messages
});
} catch (error) {
if (error instanceof AbortError) {
console.log('Query was cancelled');
} else {
throw error;
}
}
Note: Due to how Node.js handles child process aborts, you may see "AbortError" warnings in the console. These can be safely ignored. The SDK properly throws AbortError instances that you can catch.
Error Handling
const success = await parser.succeeded();
const errors = await parser.getErrors();
Custom Transformations
const customData = await parser.transform(messages => {
// Your custom logic
return processMessages(messages);
});
Session Management
The SDK provides built-in support for maintaining conversation context across multiple queries using session IDs:
Basic Session Usage
// First query - start a new session
const parser = claude()
.withModel('sonnet')
.query('Remember this number: 42');
// Get the session ID for later use
const sessionId = await parser.getSessionId();
const firstResponse = await parser.asText();
// Continue the conversation in the same session
const followUp = await claude()
.withModel('sonnet')
.withSessionId(sessionId)
.query('What number did I ask you to remember?')
.asText();
console.log(followUp); // Claude will remember "42"
Session Pattern for Complex Workflows
async function interactiveSession() {
const builder = claude()
.withModel('opus')
.skipPermissions();
// Initial context setup
const parser = builder.query('You are helping me refactor a codebase. Start by analyzing the current structure.');
const sessionId = await parser.getSessionId();
await parser.asText();
// Continue with multiple related tasks
const analysis = await builder
.withSessionId(sessionId)
.query('What are the main issues you found?')
.asText();
const plan = await builder
.withSessionId(sessionId)
.query('Create a refactoring plan to address these issues')
.asText();
const implementation = await builder
.withSessionId(sessionId)
.allowTools('Read', 'Write', 'Edit')
.query('Implement the first step of the refactoring plan')
.asResult();
return { sessionId, analysis, plan, implementation };
}
Session Best Practices
- Store session IDs: Save session IDs if you need to resume conversations later
- Context preservation: Sessions maintain full conversation history
- Tool state: File changes and tool usage are preserved within a session
- Session expiration: Sessions may expire after a period of inactivity
Logging Framework
The SDK includes a pluggable logging system:
Built-in Loggers
import { ConsoleLogger, JSONLogger, LogLevel } from '@instantlyeasy/claude-code-sdk-ts';
// Console logger with custom prefix
const logger = new ConsoleLogger(LogLevel.DEBUG, '[MyApp]');
// JSON logger for structured logging
const jsonLogger = new JSONLogger(LogLevel.INFO);
// Use with QueryBuilder
claude()
.withLogger(logger)
.query('...');
Custom Logger Implementation
import { Logger, LogEntry } from '@instantlyeasy/claude-code-sdk-ts';
class CustomLogger implements Logger {
log(entry: LogEntry): void {
// Send to your logging service
myLoggingService.send({
level: LogLevel[entry.level],
message: entry.message,
timestamp: entry.timestamp,
...entry.context
});
}
// Implement convenience methods
error(message: string, context?: Record<string, any>): void {
this.log({ level: LogLevel.ERROR, message, timestamp: new Date(), context });
}
// ... implement warn, info, debug, trace
}
Multi-Logger
import { MultiLogger, ConsoleLogger, JSONLogger } from '@instantlyeasy/claude-code-sdk-ts';
const multiLogger = new MultiLogger([
new ConsoleLogger(LogLevel.INFO),
new JSONLogger(LogLevel.DEBUG, line => fs.appendFileSync('app.log', line + '\n'))
]);
Advanced Patterns
Retry Logic
async function queryWithRetry(prompt: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await claude()
.withTimeout(30000)
.query(prompt)
.asText();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
Conditional Tool Selection
function createQuery(options: { readonly?: boolean }) {
const builder = claude();
if (options.readonly) {
builder.allowTools('Read', 'Grep', 'Glob').denyTools('Write', 'Edit');
} else {
builder.allowTools('Read', 'Write', 'Edit');
}
return builder;
}
Response Caching
const cache = new Map();
async function cachedQuery(prompt: string) {
const cacheKey = `${prompt}:${Date.now() / 60000 | 0}`; // 1-minute cache
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = await claude()
.query(prompt)
.asText();
cache.set(cacheKey, result);
return result;
}
Migration Guide
From Original API to Fluent API
The original API still works and is fully supported:
// Original API
import { query } from '@instantlyeasy/claude-code-sdk-ts';
for await (const message of query('Hello', { model: 'sonnet' })) {
// Process messages
}
// Fluent API equivalent
import { claude } from '@instantlyeasy/claude-code-sdk-ts';
await claude()
.withModel('sonnet')
.query('Hello')
.stream(async (message) => {
// Process messages
});
Common Migration Patterns
- Simple text extraction:
// Before
let text = '';
for await (const message of query('Generate text')) {
if (message.type === 'assistant') {
for (const block of message.content) {
if (block.type === 'text') {
text += block.text;
}
}
}
}
// After
const text = await claude()
.query('Generate text')
.asText();
- Tool result extraction:
// Before
const results = [];
for await (const message of query('Read files', { allowedTools: ['Read'] })) {
if (message.type === 'assistant') {
for (const block of message.content) {
if (block.type === 'tool_result' && !block.is_error) {
results.push(block.content);
}
}
}
}
// After
const results = await claude()
.allowTools('Read')
.query('Read files')
.findToolResults('Read');
- Error handling:
// Before
try {
for await (const message of query('Do something')) {
// Process and check for errors manually
}
} catch (error) {
console.error('Failed:', error);
}
// After
const parser = claude().query('Do something');
const success = await parser.succeeded();
if (!success) {
const errors = await parser.getErrors();
console.error('Failed:', errors);
}
The fluent API is designed to reduce boilerplate while maintaining the full power of the original API. You can mix and match approaches as needed for your use case.