n8n Automation

n8n API Integration Patterns: REST, GraphQL, and OAuth Workflows

DeviDevs Team
11 min read
#n8n#API integration#REST#GraphQL#OAuth#automation

n8n API Integration Patterns: REST, GraphQL, and OAuth Workflows

API integrations are fundamental to workflow automation. This guide covers essential patterns for integrating REST APIs, GraphQL endpoints, and OAuth-protected services in n8n.

REST API Patterns

Basic HTTP Request Configuration

// HTTP Request node configuration
{
  "method": "POST",
  "url": "https://api.example.com/v1/users",
  "authentication": "genericCredentialType",
  "genericAuthType": "httpHeaderAuth",
  "options": {
    "timeout": 30000,
    "redirect": {
      "followRedirects": true,
      "maxRedirects": 5
    }
  },
  "headerParameters": {
    "Content-Type": "application/json",
    "X-Request-ID": "={{ $runId }}"
  },
  "body": {
    "name": "={{ $json.name }}",
    "email": "={{ $json.email }}"
  }
}

Dynamic URL Building

// Function node for dynamic URL construction
const baseUrl = $env.API_BASE_URL;
const version = 'v2';
const resource = $json.resourceType;
const resourceId = $json.resourceId;
 
// Build URL with query parameters
const params = new URLSearchParams();
 
if ($json.filters) {
  Object.entries($json.filters).forEach(([key, value]) => {
    if (value !== null && value !== undefined) {
      params.append(key, value);
    }
  });
}
 
if ($json.pagination) {
  params.append('page', $json.pagination.page || 1);
  params.append('limit', $json.pagination.limit || 50);
}
 
const queryString = params.toString();
const url = `${baseUrl}/${version}/${resource}${resourceId ? '/' + resourceId : ''}${queryString ? '?' + queryString : ''}`;
 
return [{
  json: {
    url,
    method: $json.method || 'GET'
  }
}];

Response Handling and Error Management

// Function node for API response handling
const response = $json;
const statusCode = $response?.statusCode || response.statusCode;
 
// Define expected responses
const successCodes = [200, 201, 204];
const retryableCodes = [408, 429, 500, 502, 503, 504];
const clientErrorCodes = [400, 401, 403, 404];
 
// Success response
if (successCodes.includes(statusCode)) {
  return [{
    json: {
      success: true,
      data: response.data || response,
      metadata: {
        statusCode,
        requestId: response.headers?.['x-request-id'],
        rateLimit: {
          remaining: parseInt(response.headers?.['x-ratelimit-remaining'] || '-1'),
          reset: response.headers?.['x-ratelimit-reset']
        }
      }
    }
  }];
}
 
// Retryable error
if (retryableCodes.includes(statusCode)) {
  const retryAfter = parseInt(response.headers?.['retry-after'] || '60');
 
  return [{
    json: {
      success: false,
      retryable: true,
      statusCode,
      error: response.message || 'Server error',
      retryAfter,
      retryCount: ($json.retryCount || 0) + 1
    }
  }];
}
 
// Client error
if (clientErrorCodes.includes(statusCode)) {
  return [{
    json: {
      success: false,
      retryable: false,
      statusCode,
      error: response.message || response.error,
      details: response.errors || response.details
    }
  }];
}
 
// Unknown error
throw new Error(`Unexpected status code: ${statusCode}`);

Pagination Handling

// Function node for cursor-based pagination
const allResults = [];
let hasMore = true;
let cursor = null;
let pageCount = 0;
const maxPages = 100;
 
// Store pagination state in workflow static data
const staticData = $getWorkflowStaticData('global');
cursor = staticData.nextCursor || null;
 
// This function will be called in a loop
const processPage = async () => {
  const response = $json;
 
  // Add results from current page
  if (response.data && Array.isArray(response.data)) {
    allResults.push(...response.data);
  }
 
  // Check for next page
  hasMore = response.pagination?.hasMore || response.meta?.hasNextPage || false;
  cursor = response.pagination?.nextCursor || response.meta?.endCursor || null;
 
  // Update static data
  staticData.nextCursor = cursor;
  staticData.totalFetched = (staticData.totalFetched || 0) + response.data.length;
 
  pageCount++;
 
  return {
    hasMore: hasMore && pageCount < maxPages,
    cursor,
    resultsThisPage: response.data.length,
    totalResults: allResults.length
  };
};
 
return [{
  json: {
    paginationState: await processPage(),
    results: allResults
  }
}];
 
// For offset-based pagination
const offsetPagination = () => {
  const pageSize = 100;
  const currentPage = $json.page || 1;
  const totalCount = $json.totalCount;
 
  const offset = (currentPage - 1) * pageSize;
  const hasMore = offset + pageSize < totalCount;
 
  return {
    limit: pageSize,
    offset,
    hasMore,
    nextPage: hasMore ? currentPage + 1 : null,
    totalPages: Math.ceil(totalCount / pageSize)
  };
};

Batch API Requests

// Function node for batching API requests
const items = $input.all();
const batchSize = 50;
const batches = [];
 
// Split items into batches
for (let i = 0; i < items.length; i += batchSize) {
  batches.push(items.slice(i, i + batchSize).map(item => item.json));
}
 
// Process each batch
const results = [];
 
for (const batch of batches) {
  // Prepare batch request
  const batchRequest = {
    requests: batch.map((item, index) => ({
      id: index.toString(),
      method: 'POST',
      url: '/api/v1/resource',
      body: item
    }))
  };
 
  results.push({
    batch: batchRequest,
    batchIndex: batches.indexOf(batch)
  });
}
 
return results.map(r => ({ json: r }));

GraphQL Integration

Basic GraphQL Query

// HTTP Request node for GraphQL
{
  "method": "POST",
  "url": "https://api.example.com/graphql",
  "headers": {
    "Content-Type": "application/json",
    "Authorization": "Bearer {{ $credentials.apiToken }}"
  },
  "body": {
    "query": `
      query GetUsers($first: Int!, $after: String) {
        users(first: $first, after: $after) {
          edges {
            node {
              id
              name
              email
              createdAt
            }
            cursor
          }
          pageInfo {
            hasNextPage
            endCursor
          }
        }
      }
    `,
    "variables": {
      "first": 50,
      "after": "={{ $json.cursor || null }}"
    }
  }
}

GraphQL Mutation with Variables

// Function node for GraphQL mutation
const mutation = `
  mutation CreateOrder($input: CreateOrderInput!) {
    createOrder(input: $input) {
      order {
        id
        status
        total
        items {
          productId
          quantity
          price
        }
      }
      errors {
        field
        message
      }
    }
  }
`;
 
const variables = {
  input: {
    customerId: $json.customerId,
    items: $json.items.map(item => ({
      productId: item.productId,
      quantity: item.quantity
    })),
    shippingAddress: {
      street: $json.address.street,
      city: $json.address.city,
      state: $json.address.state,
      postalCode: $json.address.postalCode,
      country: $json.address.country
    }
  }
};
 
return [{
  json: {
    query: mutation,
    variables
  }
}];

GraphQL Error Handling

// Function node for GraphQL response handling
const response = $json;
 
// Check for GraphQL errors
if (response.errors && response.errors.length > 0) {
  const errors = response.errors.map(err => ({
    message: err.message,
    path: err.path?.join('.'),
    code: err.extensions?.code
  }));
 
  // Check if any errors are retriable
  const retryableCodes = ['INTERNAL_SERVER_ERROR', 'TIMEOUT', 'RATE_LIMITED'];
  const hasRetryable = errors.some(e => retryableCodes.includes(e.code));
 
  return [{
    json: {
      success: false,
      errors,
      retryable: hasRetryable,
      partialData: response.data  // GraphQL can return partial data with errors
    }
  }];
}
 
// Success - extract and transform data
const data = response.data;
 
return [{
  json: {
    success: true,
    data
  }
}];

GraphQL Subscriptions (via Webhook)

// Setting up GraphQL subscription via webhook
// This pattern uses a webhook endpoint to receive subscription updates
 
// Subscription setup (external service)
const subscriptionQuery = `
  subscription OnOrderUpdated($customerId: ID!) {
    orderUpdated(customerId: $customerId) {
      id
      status
      updatedAt
    }
  }
`;
 
// In n8n, create a webhook node to receive subscription events
// The external GraphQL service will POST updates to this webhook
 
// Function node to process subscription event
const event = $json;
 
if (event.type !== 'orderUpdated') {
  return [];  // Ignore other events
}
 
const orderUpdate = event.data.orderUpdated;
 
return [{
  json: {
    orderId: orderUpdate.id,
    newStatus: orderUpdate.status,
    updatedAt: orderUpdate.updatedAt,
    requiresAction: ['PAYMENT_FAILED', 'CANCELLED'].includes(orderUpdate.status)
  }
}];

OAuth Authentication Flows

OAuth 2.0 Authorization Code Flow

// Function node for OAuth state management
const crypto = require('crypto');
 
// Generate secure state parameter
const state = crypto.randomBytes(32).toString('hex');
 
// Store state for verification
const staticData = $getWorkflowStaticData('global');
staticData.oauthStates = staticData.oauthStates || {};
staticData.oauthStates[state] = {
  created: Date.now(),
  userId: $json.userId,
  returnUrl: $json.returnUrl
};
 
// Clean up old states (older than 10 minutes)
const tenMinutesAgo = Date.now() - 600000;
Object.entries(staticData.oauthStates).forEach(([key, value]) => {
  if (value.created < tenMinutesAgo) {
    delete staticData.oauthStates[key];
  }
});
 
// Build authorization URL
const params = new URLSearchParams({
  client_id: $env.OAUTH_CLIENT_ID,
  redirect_uri: $env.OAUTH_REDIRECT_URI,
  response_type: 'code',
  scope: 'read write',
  state
});
 
return [{
  json: {
    authorizationUrl: `https://oauth.provider.com/authorize?${params}`,
    state
  }
}];

OAuth Token Exchange

// Function node for token exchange
const code = $json.code;
const state = $json.state;
 
// Verify state
const staticData = $getWorkflowStaticData('global');
const savedState = staticData.oauthStates?.[state];
 
if (!savedState) {
  throw new Error('Invalid or expired state parameter');
}
 
// Clean up used state
delete staticData.oauthStates[state];
 
// Prepare token request
return [{
  json: {
    tokenEndpoint: 'https://oauth.provider.com/token',
    body: {
      grant_type: 'authorization_code',
      code,
      redirect_uri: $env.OAUTH_REDIRECT_URI,
      client_id: $env.OAUTH_CLIENT_ID,
      client_secret: $env.OAUTH_CLIENT_SECRET
    },
    userId: savedState.userId,
    returnUrl: savedState.returnUrl
  }
}];

Token Refresh Handler

// Function node for automatic token refresh
const tokenData = $json;
const now = Date.now();
 
// Check if token needs refresh (within 5 minutes of expiry)
const expiresAt = tokenData.expiresAt || (tokenData.issuedAt + tokenData.expiresIn * 1000);
const needsRefresh = expiresAt - now < 300000;  // 5 minutes
 
if (!needsRefresh) {
  return [{
    json: {
      accessToken: tokenData.accessToken,
      needsRefresh: false
    }
  }];
}
 
// Token needs refresh
if (!tokenData.refreshToken) {
  throw new Error('Token expired and no refresh token available');
}
 
return [{
  json: {
    needsRefresh: true,
    refreshToken: tokenData.refreshToken,
    tokenEndpoint: 'https://oauth.provider.com/token',
    body: {
      grant_type: 'refresh_token',
      refresh_token: tokenData.refreshToken,
      client_id: $env.OAUTH_CLIENT_ID,
      client_secret: $env.OAUTH_CLIENT_SECRET
    }
  }
}];

OAuth Token Storage

// Function node for secure token storage
const tokens = $json;
 
// Hash sensitive data for logging
const crypto = require('crypto');
const tokenHash = crypto
  .createHash('sha256')
  .update(tokens.accessToken)
  .digest('hex')
  .substring(0, 8);
 
// Prepare for storage (in database or secret manager)
const tokenRecord = {
  userId: $json.userId,
  provider: 'example_provider',
  accessToken: tokens.accessToken,  // Encrypt before storing!
  refreshToken: tokens.refreshToken,  // Encrypt before storing!
  tokenType: tokens.tokenType || 'Bearer',
  scope: tokens.scope,
  expiresAt: Date.now() + (tokens.expiresIn * 1000),
  issuedAt: Date.now(),
  // Metadata for auditing
  metadata: {
    tokenHash,
    grantType: tokens.grantType || 'authorization_code',
    createdAt: new Date().toISOString()
  }
};
 
return [{
  json: tokenRecord
}];

Rate Limiting and Throttling

Request Throttling

// Function node for request throttling
const Redis = require('ioredis');
const redis = new Redis($env.REDIS_URL);
 
const apiKey = $json.apiKey || 'default';
const endpoint = $json.endpoint;
 
// Rate limit configuration per API
const rateLimits = {
  'stripe': { requests: 100, window: 1 },      // 100 req/sec
  'sendgrid': { requests: 100, window: 1 },    // 100 req/sec
  'shopify': { requests: 2, window: 1 },       // 2 req/sec
  'hubspot': { requests: 100, window: 10 },    // 100 req/10sec
  'default': { requests: 10, window: 1 }       // 10 req/sec
};
 
const limit = rateLimits[apiKey] || rateLimits.default;
const key = `ratelimit:${apiKey}:${endpoint}`;
 
// Sliding window implementation
const now = Date.now();
const windowStart = now - (limit.window * 1000);
 
const multi = redis.multi();
multi.zremrangebyscore(key, '-inf', windowStart);
multi.zadd(key, now, `${now}-${Math.random()}`);
multi.zcard(key);
multi.expire(key, limit.window + 1);
 
const results = await multi.exec();
const requestCount = results[2][1];
 
if (requestCount > limit.requests) {
  // Calculate wait time
  const oldestInWindow = await redis.zrange(key, 0, 0, 'WITHSCORES');
  const waitMs = oldestInWindow.length > 1
    ? parseInt(oldestInWindow[1]) + (limit.window * 1000) - now
    : limit.window * 1000;
 
  return [{
    json: {
      proceed: false,
      waitMs,
      retryAfter: new Date(now + waitMs).toISOString()
    }
  }];
}
 
return [{
  json: {
    proceed: true,
    remaining: limit.requests - requestCount,
    resetAt: new Date(now + limit.window * 1000).toISOString()
  }
}];

Adaptive Rate Limiting

// Function node for adaptive rate limiting based on response headers
const response = $json;
const headers = response.headers || {};
 
// Parse rate limit headers (different APIs use different formats)
const rateLimitInfo = {
  limit: parseInt(headers['x-ratelimit-limit'] || headers['x-rate-limit-limit'] || '0'),
  remaining: parseInt(headers['x-ratelimit-remaining'] || headers['x-rate-limit-remaining'] || '0'),
  reset: parseInt(headers['x-ratelimit-reset'] || headers['x-rate-limit-reset'] || '0'),
  retryAfter: parseInt(headers['retry-after'] || '0')
};
 
// Store rate limit info for adaptive throttling
const staticData = $getWorkflowStaticData('global');
staticData.rateLimits = staticData.rateLimits || {};
 
const apiKey = $json.apiKey || 'default';
staticData.rateLimits[apiKey] = {
  ...rateLimitInfo,
  lastUpdated: Date.now()
};
 
// Calculate recommended delay
let recommendedDelay = 0;
 
if (rateLimitInfo.remaining === 0 && rateLimitInfo.reset) {
  // Wait until reset
  recommendedDelay = (rateLimitInfo.reset * 1000) - Date.now();
} else if (rateLimitInfo.remaining < rateLimitInfo.limit * 0.1) {
  // Less than 10% remaining - slow down
  recommendedDelay = 1000;  // 1 second delay
} else if (rateLimitInfo.retryAfter) {
  recommendedDelay = rateLimitInfo.retryAfter * 1000;
}
 
return [{
  json: {
    ...response,
    rateLimit: {
      ...rateLimitInfo,
      recommendedDelay: Math.max(0, recommendedDelay)
    }
  }
}];

Webhook Integration

Webhook Payload Processing

// Function node for webhook payload normalization
const payload = $json;
const headers = $request.headers;
 
// Identify webhook source
const webhookSource = (() => {
  if (headers['x-github-event']) return 'github';
  if (headers['x-shopify-hmac-sha256']) return 'shopify';
  if (headers['stripe-signature']) return 'stripe';
  if (headers['x-gitlab-event']) return 'gitlab';
  return 'unknown';
})();
 
// Normalize payload based on source
const normalizedEvent = (() => {
  switch (webhookSource) {
    case 'github':
      return {
        type: headers['x-github-event'],
        action: payload.action,
        repository: payload.repository?.full_name,
        sender: payload.sender?.login,
        data: payload
      };
 
    case 'stripe':
      return {
        type: payload.type,
        action: payload.type.split('.').pop(),
        resourceId: payload.data?.object?.id,
        livemode: payload.livemode,
        data: payload.data?.object
      };
 
    case 'shopify':
      return {
        type: headers['x-shopify-topic'],
        action: headers['x-shopify-topic']?.split('/').pop(),
        shop: headers['x-shopify-shop-domain'],
        data: payload
      };
 
    default:
      return {
        type: 'unknown',
        data: payload
      };
  }
})();
 
return [{
  json: {
    source: webhookSource,
    receivedAt: new Date().toISOString(),
    event: normalizedEvent
  }
}];

Best Practices Summary

  1. Always handle errors - Implement comprehensive error handling with retry logic
  2. Respect rate limits - Track and adapt to API rate limits
  3. Use pagination correctly - Handle both cursor and offset-based pagination
  4. Secure credentials - Never log or expose API keys and tokens
  5. Validate responses - Check response structure before processing
  6. Implement timeouts - Set appropriate timeouts for all requests
  7. Log strategically - Log enough for debugging without exposing sensitive data

By following these patterns, you can build robust, production-ready API integrations in n8n.

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.