Bỏ qua để đến Nội dung

Làm chủ 'Code Node' trong n8n: Khi Low-code cần thêm sức mạnh của JavaScript

Hướng dẫn chuyên sâu về Code Node trong n8n - khi nào cần dùng, cách viết JavaScript/TypeScript hiệu quả, và best practices để tối ưu workflow automation. Dành cho Tech Lead và developers.

Ranh giới của Low-code và Sức mạnh của Code

n8n là một trong những platform automation mạnh mẽ nhất hiện nay với hơn 400+ integrations sẵn có. Tuy nhiên, trong thực tế triển khai các workflow phức tạp, bạn sẽ nhanh chóng nhận ra một điều: low-code có giới hạn.

Khi bạn cần:

  • Xử lý cấu trúc dữ liệu phức tạp với nested objects và arrays
  • Transform data theo logic nghiệp vụ đặc thù
  • Implement thuật toán tùy chỉnh (validation, calculation, filtering)
  • Debug và troubleshoot workflow với độ chính xác cao

Đó là lúc Code Node trở thành công cụ không thể thiếu trong arsenal của một Tech Lead.

Bài viết này sẽ hướng dẫn bạn cách sử dụng Code Node một cách chuyên nghiệp - từ fundamentals đến advanced patterns được áp dụng trong production environments.

Phần 1: Khi nào cần Code Node?

Use Case 1: Data Transformation phức tạp

Tình huống: Bạn nhận response từ API với nested structure 3-4 levels, cần flatten và map sang schema của database.

Giải pháp thông thường:

  • Dùng Set Node với expressions → 20-30 fields phải config thủ công
  • Dễ sai sót, khó maintain, không scale

Với Code Node:

// Input: Complex nested API response
// Output: Flattened database-ready object

const items = $input.all();

return items.map(item => {
  const data = item.json;

  return {
    json: {
      customer_id: data.customer?.id,
      customer_name: `${data.customer?.firstName} ${data.customer?.lastName}`,
      email: data.customer?.contact?.email,
      phone: data.customer?.contact?.phone,

      // Flatten nested addresses
      shipping_address: data.order?.shipping?.address?.street,
      shipping_city: data.order?.shipping?.address?.city,
      shipping_country: data.order?.shipping?.address?.country,

      // Calculate totals from line items
      total_amount: data.order?.items?.reduce((sum, item) =>
        sum + (item.price * item.quantity), 0
      ),

      // Extract metadata
      tags: data.metadata?.tags?.join(','),
      created_at: new Date(data.timestamp).toISOString()
    }
  };
});

Lợi ích:

  • 1 node thay vì 5-10 nodes
  • Dễ đọc, dễ maintain
  • Version control friendly (code có thể track changes)

Use Case 2: Business Logic đặc thù

Tình huống: Tính toán commission cho sales team theo quy tắc phức tạp.

// Commission calculation với multiple tiers và conditions
const items = $input.all();

function calculateCommission(sales) {
  const amount = sales.total_amount;
  const tier = sales.seller_tier;
  const isRecurring = sales.subscription_type === 'recurring';

  let baseRate = 0;

  // Tier-based commission
  switch(tier) {
    case 'platinum': baseRate = 0.15; break;
    case 'gold': baseRate = 0.12; break;
    case 'silver': baseRate = 0.10; break;
    default: baseRate = 0.08;
  }

  // Bonus for recurring
  if (isRecurring) {
    baseRate += 0.03;
  }

  // Volume bonus
  if (amount > 100000) {
    baseRate += 0.02;
  }

  return Math.round(amount * baseRate);
}

return items.map(item => ({
  json: {
    ...item.json,
    commission: calculateCommission(item.json),
    calculated_at: new Date().toISOString()
  }
}));

Use Case 3: Data Validation & Enrichment

// Validate và enrich data trước khi insert vào database
const items = $input.all();

function validateEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

function validatePhone(phone) {
  // Vietnamese phone format: 0[3|5|7|8|9]xxxxxxxx
  const regex = /^0[3|5|7|8|9][0-9]{8}$/;
  return regex.test(phone?.replace(/[\s-]/g, ''));
}

return items.map(item => {
  const data = item.json;
  const errors = [];

  // Validation
  if (!validateEmail(data.email)) {
    errors.push('Invalid email format');
  }

  if (!validatePhone(data.phone)) {
    errors.push('Invalid phone format');
  }

  if (!data.full_name || data.full_name.length < 3) {
    errors.push('Name too short');
  }

  // Enrichment
  return {
    json: {
      ...data,
      // Normalize data
      email: data.email?.toLowerCase().trim(),
      phone: data.phone?.replace(/[\s-]/g, ''),
      full_name: data.full_name?.trim(),

      // Add metadata
      validation_status: errors.length === 0 ? 'valid' : 'invalid',
      validation_errors: errors,
      processed_at: new Date().toISOString(),

      // Generate ID if needed
      record_id: data.id || `REC_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
    }
  };
});

Phần 2: JavaScript/TypeScript Fundamentals trong n8n

Hiểu về Execution Context

n8n Code Node chạy trong một sandboxed environment với một số điểm cần lưu ý:

1. Input Data Access:

// Lấy tất cả items từ node trước
const items = $input.all();

// Lấy item đầu tiên
const firstItem = $input.first();

// Lấy item tại index cụ thể
const item = $input.item(0);

// Access previous node by name
const specificNodeData = $('Node_Name').all();

2. Environment Variables:

// Access workflow env variables
const apiKey = $env.API_KEY;
const environment = $env.ENVIRONMENT;

3. Node Parameters:

// Access current node's parameters
const mode = $parameter.mode;

TypeScript Support

n8n hỗ trợ TypeScript - giúp code an toàn hơn và IDE autocomplete:

// Enable TypeScript mode in Code Node settings

interface Customer {
  id: string;
  name: string;
  email: string;
  tier: 'basic' | 'premium' | 'enterprise';
}

interface Order {
  orderId: string;
  customer: Customer;
  items: Array<{
    sku: string;
    quantity: number;
    price: number;
  }>;
  total: number;
}

// Type-safe data processing
const items = $input.all();

return items.map(item => {
  const order = item.json as Order;

  // IDE sẽ suggest properties và catch errors
  return {
    json: {
      customer_tier: order.customer.tier,
      order_value: order.total,
      item_count: order.items.length,
      average_item_price: order.total / order.items.length
    }
  };
});

Working with Dates

// Date manipulation trong n8n
const items = $input.all();

return items.map(item => {
  const createdAt = new Date(item.json.created_at);
  const now = new Date();

  // Calculate age in days
  const ageInDays = Math.floor((now - createdAt) / (1000 * 60 * 60 * 24));

  // Format dates
  const formattedDate = createdAt.toLocaleDateString('vi-VN');
  const isoDate = createdAt.toISOString();

  return {
    json: {
      ...item.json,
      age_in_days: ageInDays,
      formatted_date: formattedDate,
      iso_date: isoDate,
      is_recent: ageInDays <= 7
    }
  };
});

Phần 3: Advanced Patterns & Real-world Examples

Pattern 1: Batch Processing với Error Handling

// Process large datasets với proper error handling
const items = $input.all();
const results = [];
const errors = [];

for (const item of items) {
  try {
    const data = item.json;

    // Complex processing logic
    const processed = {
      id: data.id,
      processed_data: performComplexOperation(data),
      status: 'success'
    };

    results.push({ json: processed });

  } catch (error) {
    errors.push({
      json: {
        id: item.json.id,
        error: error.message,
        status: 'failed',
        timestamp: new Date().toISOString()
      }
    });
  }
}

function performComplexOperation(data) {
  // Your complex logic here
  if (!data.required_field) {
    throw new Error('Missing required field');
  }

  return {
    transformed: data.value * 2,
    validated: true
  };
}

// Return both successful và failed items
return [...results, ...errors];

Pattern 2: API Response Pagination Handler

// Xử lý paginated API responses
const allItems = $input.all();
let allRecords = [];

// Giả sử API trả về paginated data
for (const item of allItems) {
  const response = item.json;

  // Extract records from current page
  if (response.data && Array.isArray(response.data)) {
    allRecords = allRecords.concat(response.data);
  }
}

// Deduplicate based on ID
const uniqueRecords = allRecords.reduce((acc, record) => {
  if (!acc.find(r => r.id === record.id)) {
    acc.push(record);
  }
  return acc;
}, []);

// Transform và return
return uniqueRecords.map(record => ({
  json: {
    id: record.id,
    name: record.name,
    email: record.email,
    total_records: uniqueRecords.length
  }
}));

Pattern 3: Dynamic SQL Query Builder

// Build dynamic SQL queries based on conditions
const items = $input.all();

function buildWhereClause(filters) {
  const conditions = [];

  if (filters.status) {
    conditions.push(`status = '${filters.status}'`);
  }

  if (filters.minAmount) {
    conditions.push(`amount >= ${filters.minAmount}`);
  }

  if (filters.dateFrom) {
    conditions.push(`created_at >= '${filters.dateFrom}'`);
  }

  if (filters.tags && filters.tags.length > 0) {
    const tagList = filters.tags.map(t => `'${t}'`).join(',');
    conditions.push(`tags && ARRAY[${tagList}]`);
  }

  return conditions.length > 0 ? conditions.join(' AND ') : '1=1';
}

return items.map(item => {
  const filters = item.json;

  const query = `
    SELECT *
    FROM orders
    WHERE ${buildWhereClause(filters)}
    ORDER BY created_at DESC
    LIMIT 100
  `;

  return {
    json: {
      query: query.trim(),
      filters: filters
    }
  };
});

Pattern 4: Data Aggregation & Analytics

// Aggregate data cho reporting
const items = $input.all();

// Group by category và calculate metrics
const analytics = items.reduce((acc, item) => {
  const data = item.json;
  const category = data.category || 'uncategorized';

  if (!acc[category]) {
    acc[category] = {
      count: 0,
      total_revenue: 0,
      items: []
    };
  }

  acc[category].count += 1;
  acc[category].total_revenue += data.amount || 0;
  acc[category].items.push(data.id);

  return acc;
}, {});

// Transform to array và sort
const results = Object.entries(analytics)
  .map(([category, stats]) => ({
    category,
    ...stats,
    average_revenue: stats.total_revenue / stats.count
  }))
  .sort((a, b) => b.total_revenue - a.total_revenue);

return results.map(r => ({ json: r }));

Phần 4: Best Practices khi viết Code Node

1. Luôn Return đúng format

// ❌ WRONG - Return plain object
return { name: 'John' };

// ✅ CORRECT - Return array of items với json property
return [{ json: { name: 'John' } }];

// ✅ CORRECT - Map existing items
return items.map(item => ({
  json: { ...item.json, processed: true }
}));

2. Handle Edge Cases

const items = $input.all();

// ❌ WRONG - Không check empty
const firstItem = items[0].json;

// ✅ CORRECT - Defensive programming
if (!items || items.length === 0) {
  return [{ json: { error: 'No input data' } }];
}

const data = items[0]?.json || {};
const email = data.email?.toLowerCase() || '';
const total = data.items?.reduce((sum, i) => sum + i.price, 0) || 0;

3. Sử dụng Functions để tái sử dụng logic

// ✅ Clean code với functions
const items = $input.all();

function normalizeEmail(email) {
  return email?.toLowerCase().trim() || '';
}

function formatCurrency(amount, currency = 'VND') {
  return new Intl.NumberFormat('vi-VN', {
    style: 'currency',
    currency: currency
  }).format(amount);
}

function generateSlug(text) {
  return text
    ?.toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[đĐ]/g, 'd')
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '') || '';
}

return items.map(item => ({
  json: {
    email: normalizeEmail(item.json.email),
    formatted_price: formatCurrency(item.json.price),
    slug: generateSlug(item.json.title)
  }
}));

4. Logging & Debugging

const items = $input.all();

// Log để debug (hiển thị trong execution log)
console.log('Processing items:', items.length);
console.log('First item:', JSON.stringify(items[0], null, 2));

// Log errors với context
try {
  const result = complexOperation(items[0].json);
  console.log('Operation success:', result);
} catch (error) {
  console.error('Operation failed:', {
    error: error.message,
    stack: error.stack,
    input: items[0].json
  });
  throw error; // Re-throw để workflow biết có lỗi
}

5. Performance Optimization

// ❌ WRONG - Inefficient loop
const items = $input.all();
let results = [];

for (let i = 0; i < items.length; i++) {
  for (let j = 0; j < items.length; j++) {
    if (items[i].json.id === items[j].json.related_id) {
      results.push(items[i]);
    }
  }
}

// ✅ CORRECT - Use Map for O(1) lookup
const items = $input.all();
const itemMap = new Map(
  items.map(item => [item.json.id, item])
);

const results = items
  .filter(item => itemMap.has(item.json.related_id))
  .map(item => ({
    json: {
      ...item.json,
      related: itemMap.get(item.json.related_id)?.json
    }
  }));

return results;

6. Memory Management cho Large Datasets

// Process large datasets in chunks
const items = $input.all();
const CHUNK_SIZE = 100;

function processChunk(chunk) {
  return chunk.map(item => ({
    json: {
      // Your transformation logic
      processed: true,
      ...item.json
    }
  }));
}

// Process in batches
const results = [];
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
  const chunk = items.slice(i, i + CHUNK_SIZE);
  const processed = processChunk(chunk);
  results.push(...processed);

  // Optional: Log progress
  if (i % 1000 === 0) {
    console.log(`Processed ${i}/${items.length} items`);
  }
}

return results;

Phần 5: Tips tối ưu Workflow

Tip 1: Kết hợp Code Node với IF Node

// Code Node: Calculate score
const items = $input.all();

return items.map(item => {
  const lead = item.json;

  let score = 0;

  // Scoring logic
  if (lead.company_size > 100) score += 30;
  if (lead.industry === 'technology') score += 20;
  if (lead.budget > 50000) score += 25;
  if (lead.timeline === 'immediate') score += 25;

  return {
    json: {
      ...lead,
      lead_score: score,
      priority: score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low'
    }
  };
});

// Sau đó dùng IF Node để route based on priority

Tip 2: Tái sử dụng Code với Function Items

// Tạo library functions trong một Code Node riêng
// Output có thể dùng ở nhiều workflows

const libraries = {
  validation: {
    email: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
    phone: (phone) => /^0[3|5|7|8|9][0-9]{8}$/.test(phone?.replace(/[\s-]/g, '')),
    url: (url) => {
      try {
        new URL(url);
        return true;
      } catch {
        return false;
      }
    }
  },

  formatter: {
    currency: (amount, locale = 'vi-VN', currency = 'VND') =>
      new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount),

    date: (date, locale = 'vi-VN') =>
      new Date(date).toLocaleDateString(locale),

    slug: (text) =>
      text?.toLowerCase()
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '')
        .replace(/[đĐ]/g, 'd')
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-+|-+$/g, '')
  }
};

return [{ json: libraries }];

Tip 3: Use Code Node cho Complex Conditions

Thay vì chain nhiều IF nodes:

// Thay vì 5-6 IF nodes, dùng 1 Code Node
const items = $input.all();

function determineAction(customer) {
  // Complex decision tree
  if (customer.subscription_status === 'cancelled') {
    if (customer.lifetime_value > 10000) {
      return 'winback_vip';
    } else if (customer.last_purchase_days < 90) {
      return 'winback_recent';
    } else {
      return 'archive';
    }
  }

  if (customer.subscription_status === 'active') {
    if (customer.usage_score < 30) {
      return 'engagement_campaign';
    } else if (customer.months_subscribed > 12) {
      return 'upsell_campaign';
    } else {
      return 'nurture';
    }
  }

  return 'default';
}

return items.map(item => ({
  json: {
    ...item.json,
    action: determineAction(item.json),
    processed_at: new Date().toISOString()
  }
}));

// Sau đó dùng Switch Node để route theo action

Tip 4: Testing & Validation trong Development

// Add validation checks khi develop
const items = $input.all();

// Development mode checks
const isDev = $env.ENVIRONMENT === 'development';

if (isDev) {
  // Validate input structure
  const requiredFields = ['id', 'email', 'name'];
  const firstItem = items[0]?.json || {};

  const missingFields = requiredFields.filter(field => !firstItem[field]);

  if (missingFields.length > 0) {
    console.warn('Missing required fields:', missingFields);
  }

  // Log sample data
  console.log('Sample input:', JSON.stringify(firstItem, null, 2));
}

// Your processing logic
const results = items.map(item => ({
  json: {
    // ... transformation
  }
}));

if (isDev) {
  console.log('Sample output:', JSON.stringify(results[0], null, 2));
}

return results;

Phần 6: Common Pitfalls & Solutions

Pitfall 1: Không xử lý null/undefined

// ❌ WRONG
const name = item.json.customer.name.toUpperCase();

// ✅ CORRECT
const name = item.json?.customer?.name?.toUpperCase() || 'UNKNOWN';

Pitfall 2: Mutate original data

// ❌ WRONG - Mutate original
const item = items[0];
item.json.processed = true;

// ✅ CORRECT - Create new object
const result = {
  json: {
    ...item.json,
    processed: true
  }
};

Pitfall 3: Không return đúng format

// ❌ WRONG
return { success: true };

// ✅ CORRECT
return [{ json: { success: true } }];

Pitfall 4: Blocking operations

// ❌ WRONG - n8n Code Node không support async/await trong some contexts
const response = await fetch('https://api.example.com');

// ✅ CORRECT - Dùng HTTP Request Node thay vì fetch trong Code Node
// Hoặc return data để process ở node tiếp theo

Conclusion: Code Node là công cụ mạnh mẽ trong tay Tech Lead

Code Node biến n8n từ một low-code platform thành một orchestration powerhouse có khả năng xử lý mọi business logic phức tạp.

Key Takeaways:

  1. Biết khi nào dùng: Không phải mọi transformation đều cần Code Node. Dùng khi logic phức tạp, cần flexibility, hoặc để optimize workflow.
  2. Write clean code: Functions, error handling, defensive programming - apply coding best practices như bạn làm trong production codebase.
  3. Type safety: Sử dụng TypeScript khi có thể để catch errors sớm và improve developer experience.
  4. Performance matters: Với large datasets, optimize algorithms và consider memory management.
  5. Testing mindset: Validate inputs, handle edge cases, log appropriately để debug dễ dàng.
  6. Reusability: Extract common logic thành functions, consider tạo library Code Nodes để reuse across workflows.

Code Node không phải là "escape hatch" khi low-code thất bại - nó là công cụ professional để build production-grade automation workflows với sự kiểm soát tuyệt đối.

Khi bạn master được Code Node, bạn sẽ có khả năng transform bất kỳ business process nào thành automated workflow, dù phức tạp đến đâu.

Related Resources