| layout | default |
|---|---|
| title | Chapter 4: N8nApiClient - Communicating with n8n |
| parent | n8n-MCP Tutorial |
| nav_order | 4 |
Welcome to the communication layer! In Chapter 3, we explored how the HTTP server manages MCP sessions. Now let's dive into the N8nApiClient—the sophisticated diplomat that handles all communication with n8n instances.
Think of the N8nApiClient as an expert translator and negotiator. n8n speaks its own REST API language, and the N8nApiClient not only translates MCP requests into n8n API calls but also handles negotiations, retries, error recovery, and version compatibility.
n8n API communication isn't trivial. Consider these challenges:
- Version Compatibility - n8n evolves rapidly, API changes between versions
- Authentication - Secure API key handling and renewal
- Rate Limiting - n8n instances have request limits
- Network Reliability - Temporary outages, timeouts, connection issues
- Data Transformation - Converting between MCP and n8n data formats
- Error Handling - Meaningful error messages from cryptic HTTP responses
The N8nApiClient solves all these elegantly.
The client is built around Axios with extensive customization:
interface N8nApiClientConfig {
baseUrl: string; // n8n instance URL
apiKey: string; // Authentication key
timeout?: number; // Request timeout (default: 30s)
maxRetries?: number; // Retry attempts (default: 3)
}
class N8nApiClient {
private client: AxiosInstance;
private maxRetries: number;
private versionInfo: N8nVersionInfo | null = null;
}One of the client's most sophisticated features is version-aware communication:
// Automatically detects n8n version and adapts behavior
const version = await client.getVersion();
// Returns: { version: "2.2.3", major: 2, minor: 2, patch: 3 }
// Different versions may have different API behaviors
if (version.greaterThan('2.0.0')) {
// Use new API endpoints
} else {
// Fallback to legacy endpoints
}This enables n8n-MCP to work across different n8n versions without manual configuration.
The client uses Axios interceptors for cross-cutting concerns:
// Request interceptor - adds authentication and logging
this.client.interceptors.request.use((config) => {
// Add API key header
config.headers['X-N8N-API-KEY'] = this.apiKey;
// Log outgoing requests
logger.debug(`n8n API Request: ${config.method?.toUpperCase()} ${config.url}`);
return config;
});
// Response interceptor - handles errors and logging
this.client.interceptors.response.use(
(response) => {
logger.debug(`n8n API Response: ${response.status}`);
return response;
},
(error) => {
// Transform n8n errors into meaningful messages
const n8nError = handleN8nApiError(error);
logN8nError(n8nError, 'n8n API Response');
return Promise.reject(n8nError);
}
);The client provides methods for all n8n workflow operations:
// Create workflow
const workflow = await client.createWorkflow({
name: 'Email Marketing Automation',
nodes: [...],
connections: {...},
settings: {...}
});
// Read workflow
const workflow = await client.getWorkflow('workflow-id');
// Update workflow
const updated = await client.updateWorkflow('workflow-id', {
name: 'Updated Marketing Workflow',
nodes: modifiedNodes
});
// Delete workflow
await client.deleteWorkflow('workflow-id');// List workflows with filtering
const workflows = await client.listWorkflows({
limit: 50,
offset: 0,
tags: ['marketing', 'automation']
});
// Activate/deactivate workflows
await client.activateWorkflow('workflow-id');
await client.deactivateWorkflow('workflow-id');Workflow executions are central to n8n's operation:
// Start workflow execution
const execution = await client.executeWorkflow('workflow-id', {
data: inputData // Optional input data
});
// Get execution status and results
const execution = await client.getExecution(executionId);
// List executions with filtering
const executions = await client.listExecutions({
workflowId: 'workflow-id',
limit: 10,
status: 'success' // success, error, running, waiting
});
// Delete old executions
await client.deleteExecution(executionId);Secure handling of n8n credentials:
// List available credentials
const credentials = await client.listCredentials();
// Get specific credential
const credential = await client.getCredential('credential-id');
// Create new credential
const newCredential = await client.createCredential({
name: 'Gmail Integration',
type: 'gmailOAuth2',
data: {
clientId: '...',
clientSecret: '...'
}
});n8n uses tags for organization:
// List all tags
const tags = await client.listTags();
// Create new tag
const tag = await client.createTag({
name: 'Marketing',
color: '#FF6B6B'
});The client implements sophisticated error handling:
class N8nApiError extends Error {
statusCode: number;
errorCode: string;
details?: any;
constructor(message: string, statusCode: number, errorCode: string, details?: any) {
super(message);
this.statusCode = statusCode;
this.errorCode = errorCode;
this.details = details;
}
}
// Usage
try {
await client.createWorkflow(workflowData);
} catch (error) {
if (error instanceof N8nApiError) {
switch (error.errorCode) {
case 'WORKFLOW_EXISTS':
// Handle duplicate workflow
break;
case 'INVALID_CREDENTIALS':
// Handle auth failure
break;
default:
// Handle other errors
}
}
}function handleN8nApiError(error: any): N8nApiError {
if (error.response) {
// Server responded with error status
const { status, data } = error.response;
switch (status) {
case 400:
return new N8nApiError('Invalid request', 400, 'INVALID_REQUEST', data);
case 401:
return new N8nApiError('Authentication failed', 401, 'AUTH_FAILED');
case 403:
return new N8nApiError('Access denied', 403, 'ACCESS_DENIED');
case 404:
return new N8nApiError('Resource not found', 404, 'NOT_FOUND');
case 429:
return new N8nApiError('Rate limit exceeded', 429, 'RATE_LIMIT');
default:
return new N8nApiError('Server error', status, 'SERVER_ERROR', data);
}
} else if (error.request) {
// Network error
return new N8nApiError('Network error', 0, 'NETWORK_ERROR', error.message);
} else {
// Other error
return new N8nApiError('Request error', 0, 'REQUEST_ERROR', error.message);
}
}Network issues are handled gracefully:
// Automatic retry with exponential backoff
const response = await this.executeWithRetry(async () => {
return this.client.get('/workflows');
});
private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
// Don't retry on client errors (4xx)
if (error.statusCode >= 400 && error.statusCode < 500) {
throw error;
}
// Don't retry on last attempt
if (attempt === this.maxRetries) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s, 8s...
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}The client handles complex data transformations between MCP and n8n formats:
// n8n requires specific workflow format
function cleanWorkflowForCreate(workflow: any) {
return {
...workflow,
// Remove client-side fields
id: undefined,
createdAt: undefined,
updatedAt: undefined,
// Ensure required fields
name: workflow.name || 'Untitled Workflow',
nodes: workflow.nodes || [],
connections: workflow.connections || {},
// Version-specific transformations
...(version.greaterThan('2.0.0') ? {
settings: {
...workflow.settings,
executionOrder: workflow.settings?.executionOrder || 'v1'
}
} : {})
};
}// Handle version-specific settings
function cleanSettingsForVersion(settings: any, version: N8nVersionInfo) {
if (version.greaterThan('2.1.0')) {
// New version supports additional settings
return {
...settings,
timezone: settings.timezone || 'UTC',
saveExecutionProgress: settings.saveExecutionProgress ?? true
};
} else {
// Legacy version - remove unsupported settings
const { timezone, saveExecutionProgress, ...legacySettings } = settings;
return legacySettings;
}
}For high-performance deployments:
// Configure connection pooling
const clientConfig = {
baseURL: n8nApiUrl,
timeout: 30000,
maxSockets: 10, // Connection pool size
keepAlive: true, // Persistent connections
maxFreeSockets: 5, // Keep some connections idle
freeSocketTimeout: 30000 // Close idle connections after 30s
};Built-in monitoring capabilities:
// Request metrics
class N8nApiClient {
private metrics = {
requestsTotal: 0,
requestsByEndpoint: new Map<string, number>(),
errorsByType: new Map<string, number>(),
responseTimeHistogram: new Histogram()
};
private recordRequest(endpoint: string, duration: number, success: boolean) {
this.metrics.requestsTotal++;
this.metrics.requestsByEndpoint.set(
endpoint,
(this.metrics.requestsByEndpoint.get(endpoint) || 0) + 1
);
if (!success) {
// Record error type
}
this.metrics.responseTimeHistogram.observe(duration);
}
}The client is designed for easy testing:
// Mock for unit tests
const mockClient = {
createWorkflow: jest.fn().mockResolvedValue(mockWorkflow),
getWorkflow: jest.fn().mockResolvedValue(mockWorkflow),
listWorkflows: jest.fn().mockResolvedValue([mockWorkflow])
};
// Integration tests with real n8n instance
describe('N8nApiClient Integration', () => {
let client: N8nApiClient;
beforeAll(() => {
client = new N8nApiClient({
baseUrl: process.env.N8N_TEST_URL!,
apiKey: process.env.N8N_TEST_API_KEY!
});
});
it('should create and retrieve workflow', async () => {
const workflow = await client.createWorkflow(testWorkflowData);
expect(workflow.id).toBeDefined();
const retrieved = await client.getWorkflow(workflow.id);
expect(retrieved.name).toBe(testWorkflowData.name);
});
});const client = new N8nApiClient({
baseUrl: 'https://my-n8n-instance.com',
apiKey: 'my-api-key'
});
const workflows = await client.listWorkflows();// For multi-tenant applications
function createClientForInstance(context: InstanceContext): N8nApiClient {
return new N8nApiClient({
baseUrl: context.n8nApiUrl,
apiKey: context.n8nApiKey,
timeout: context.n8nApiTimeout || 30000,
maxRetries: context.n8nApiMaxRetries || 3
});
}async function createWorkflowWithRetry(client: N8nApiClient, data: any) {
try {
return await client.createWorkflow(data);
} catch (error) {
if (error.errorCode === 'WORKFLOW_EXISTS') {
// Generate unique name and retry
const uniqueData = {
...data,
name: `${data.name} (${Date.now()})`
};
return await client.createWorkflow(uniqueData);
}
throw error;
}
}Typical performance characteristics:
- Connection Time: 50-200ms (depends on network)
- Simple Operations: 100-300ms (list workflows, get workflow)
- Complex Operations: 500-2000ms (create/update workflows)
- Concurrent Requests: Handles 50+ simultaneous operations
- Memory Usage: ~5-10MB per client instance
The client implements security best practices:
// Secure API key handling
class N8nApiClient {
private apiKey: string;
constructor(config: N8nApiClientConfig) {
// Validate API key format
if (!config.apiKey || config.apiKey.length < 10) {
throw new Error('Invalid API key format');
}
this.apiKey = config.apiKey;
// Configure secure headers
this.client.defaults.headers.common = {
'X-N8N-API-KEY': this.apiKey,
'User-Agent': 'n8n-mcp/2.33.0',
'Accept': 'application/json',
'Content-Type': 'application/json'
};
}
}Congratulations! You now understand how the N8nApiClient serves as the sophisticated communication layer between n8n-MCP and n8n instances, handling all the complexities of API communication gracefully.
In the next chapter, we'll explore the data storage layer that powers fast lookups of n8n's extensive node library.