How to Build Custom n8n Nodes: A Developer’s Guide to Extending Workflows

How to Build Custom n8n Nodes: A Developer’s Guide to Extending Workflows

n8n comes with hundreds of built-in nodes, but sometimes you need to connect to an API that isn’t supported. That’s where custom nodes come in. This guide walks you through building, testing, and publishing a custom n8n node from scratch.

What Are n8n Nodes?

Nodes are the building blocks of n8n workflows. Each node performs a specific function: HTTP request, database query, data transformation, or API integration. Custom nodes let you extend n8n to work with any service.

Examples of custom nodes you might build:

  • Integration with your company’s internal API
  • Specialized data transformation logic
  • Custom authentication method (OAuth variant)
  • Proprietary database connector

Prerequisites

  • Node.js v18+ installed
  • TypeScript basics (types, interfaces)
  • n8n installed (either cloud or self-hosted)
  • Git for version control

No prior n8n node development experience required — this guide covers everything.

Step 1: Set Up Development Environment

Install n8n-node-dev CLI

The official scaffolding tool creates the node structure for you.

npm install -g n8n-node-dev

Create Node Project

mkdir my-n8n-node
cd my-n8n-node
n8n-node-dev init

Answer the prompts:

  • Node name: My Custom API (or your service name)
  • Node type: regular (or trigger if it starts workflows)
  • Version: 1.0.0
  • License: MIT (recommended for community)

This creates:

my-n8n-node/
├── nodes/
│   └── MyCustomApi/
│       ├── MyCustomApi.node.ts
│       ├── MyCustomApi.ts
│       └── credentials/
│           └── MyCustomApiApi.credentials.ts
├── package.json
└── tsconfig.json

Step 2: Define Credentials (API Keys)

Most integrations need authentication. Open nodes/MyCustomApi/credentials/MyCustomApiApi.credentials.ts:

import { IAuthenticateGeneric } from 'n8n-workflow';

export class MyCustomApiApi implements IAuthenticateGeneric {
  name = 'myCustomApiApi';
  stage = 'target';
  properties = [
    {
      displayName: 'API Key',
      name: 'apiKey',
      type: 'string',
      typeOptions: { password: true },
      default: '',
      required: true,
    },
    {
      displayName: 'Base URL',
      name: 'baseUrl',
      type: 'string',
      default: 'https://api.myservice.com/v1',
      required: true,
    },
  ];
}

This creates two credential fields in n8n UI: API Key (hidden) and Base URL.

Step 3: Implement the Node Logic

Open nodes/MyCustomApi/MyCustomApi.node.ts. This is where your business logic lives.

Structure Overview

import { IExecuteFunctions } from 'n8n-workflow';
import { MyCustomApiApi } from './credentials';

export class MyCustomApi implements IExecuteFunctions {
  async execute(this: IExecuteFunctions): Promise<NodeApiOutputData[]> {
    // 1. Get credentials
    const credentials = await this.getCredentials('myCustomApiApi');

    // 2. Get input data from previous node
    const inputData = this.getInputData();
    const resource = this.getNodeParameter('resource', 0); // e.g., 'user', 'order'
    const operation = this.getNodeParameter('operation', 0); // e.g., 'create', 'get'

    // 3. Build API request
    const url = `${credentials.baseUrl}/${resource}`;
    const options = {
      method: operation === 'create' ? 'POST' : 'GET',
      body: operation === 'create' ? inputData[0].json : undefined,
      headers: { 'Authorization': `Bearer ${credentials.apiKey}` },
    };

    // 4. Make HTTP request (n8n provides this.helpers.request)
    const response = await this.helpers.httpRequest(options);

    // 5. Return data to workflow
    return [{ json: response.data }];
  }
}

Step 4: Add Resource and Operation Options

You’ll want users to select what they’re doing (e.g., “User → Create”). Define these in MyCustomApi.ts:

import { IBasicNode, IExecuteFunctions } from 'n8n-workflow';
import { MyCustomApi } from './MyCustomApi.node';

export class MyCustomApi implements IBasicNode {
  description: INodeTypeDescription = {
    displayName: 'My Custom API',
    name: 'myCustomApi',
    icon: 'fa:plug',
    group: ['transform'],
    version: 1,
    subtitle: '={{$parameter["resource"]}}',
    description: 'Integrate with MyCustomService API',
    defaults: { name: 'My Custom API' },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [{ name: 'myCustomApiApi', required: true }],
    properties: [
      {
        displayName: 'Resource',
        name: 'resource',
        type: 'options',
        options: [
          { name: 'User', value: 'user' },
          { name: 'Order', value: 'order' },
        ],
        default: 'user',
      },
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        options: [
          { name: 'Create', value: 'create' },
          { name: 'Get', value: 'get' },
          { name: 'Update', value: 'update' },
          { name: 'Delete', value: 'delete' },
        ],
        default: 'create',
      },
      // Add other parameters as needed
    ],
  };

  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    return await new MyCustomApi().execute.call(this);
  }
}

Step 5: Test Locally

Link your node to a local n8n instance:

# In your node project
npm link

# In n8n project (or self-hosted setup)
cd /path/to/n8d
npm link my-n8n-node

Start n8n in development mode:

npm run dev

Open n8n UI (http://localhost:5678) — your node should appear in the node palette under “Custom” category.

Debugging Tips

  • Add console.log() statements in your node code
  • Check n8n logs: docker logs n8n or terminal output
  • Use this.helpers.httpRequest with rejectUnauthorized: false for self-signed certs during testing
  • Test with mock data first before hitting real API

Step 6: Handle Errors and Retries

n8n expects proper error handling. Wrap HTTP calls:

try {
  const response = await this.helpers.httpRequest(options);
  if (response.status >= 300) {
    throw new Error(`API returned ${response.status}: ${response.body}`);
  }
  return response.data;
} catch (error) {
  // Optional: implement retry logic manually or let n8n handle
  throw error; // n8n will mark execution as failed
}

For transient errors (rate limits, network blips), implement exponential backoff retry (3 attempts, delays of 1s, 2s, 4s).

Step 7: Package for Distribution

When your node is ready to share (or install in production):

npm run build   # Compile TypeScript to JavaScript
npm pack         # Creates .tgz package

To install in another n8n instance:

npm install ./my-n8n-node-1.0.0.tgz

Step 8: Publish to n8n Community Nodes (Optional)

If you want to share with the world:

  1. Create GitHub repo (public)
  2. Add n8n-nodes topic
  3. Submit via n8n community form: https://docs.n8n.io/integrations/community-nodes/
  4. Maintainers will review (typically 1-2 weeks)

Once approved, your node appears in n8n’s “Community Nodes” section and users can install via UI.

Real Example: Building a Simple JSONPlaceholder Node

Let’s build a node that fetches posts from JSONPlaceholder (a fake API for testing):

MyJsonPlaceholderApiApi.credentials.ts (no auth needed, just base URL)

export class MyJsonPlaceholderApiApi implements IAuthenticateGeneric {
  name = 'myJsonPlaceholderApi';
  stage = 'target';
  properties = [
    {
      displayName: 'Base URL',
      name: 'baseUrl',
      type: 'string',
      default: 'https://jsonplaceholder.typicode.com',
      required: true,
    },
  ];
}

MyJsonPlaceholderApi.node.ts

async execute() {
  const credentials = await this.getCredentials('myJsonPlaceholderApi');
  const resource = this.getNodeParameter('resource', 0); // 'posts' or 'comments'
  const id = this.getNodeParameter('id', 0, null); // optional

  const url = `${credentials.baseUrl}/${resource}${id ? '/' + id : ''}`;
  const response = await this.helpers.httpRequest({ url, method: 'GET' });
  return [{ json: response.data }];
}

MyJsonPlaceholderApi.ts

properties: [
  { displayName: 'Resource', name: 'resource', type: 'options', options: [
    { name: 'Posts', value: 'posts' },
    { name: 'Comments', value: 'comments' },
  ]},
  { displayName: 'ID (optional)', name: 'id', type: 'string', required: false },
]

That’s it! You’ve built a working node in ~50 lines of code.

Advanced Features

Pagination

If API returns paginated results, implement handlePages:

async execute(this: IExecuteFunctions): Promise<NodeApiOutputData[]> {
  const returnData: any[] = [];
  let response = await this.getFirstPage();
  returnData.push(...response.data);
  
  while (response.hasMore) {
    response = await this.getNextPage(response.nextUrl);
    returnData.push(...response.data);
  }
  
  return returnData.map(item => ({ json: item }));
}

Binary Data (Files)

For file downloads/uploads, return binaryData property and set MIME type.

Webhooks (Trigger Nodes)

If building a trigger (event-driven node), you need to implement webhook endpoint that n8n provides via this.helpers.handleWebhook.

Testing Your Node

Write unit tests with Jest (included with scaffold):

npm test

Cover at least:

  • Happy path (successful API call)
  • Error handling (API returns 400/500)
  • Input validation
  • Credential loading

Deploying to Production

To use your custom node in production n8n:

  1. Build: npm run build (creates dist/)
  2. Package: npm pack → get .tgz file
  3. Install: On production n8n host: npm install /path/to/package.tgz
  4. Restart n8n: pm2 restart n8n or docker restart
  5. The node will appear automatically in the palette.

    Maintenance Tips

    • Pin API dependencies in package.json (avoid breaking changes)
    • Use semantic versioning (MAJOR.MINOR.PATCH)
    • Document parameters in node description (they appear as tooltips in UI)
    • Handle rate limiting gracefully (retry with backoff)
    • Keep credentials secure (never log API keys)

    Conclusion

    Building custom n8n nodes is straightforward once you understand the pattern. With this guide, you can integrate any API into n8n’s visual workflow environment.

    Key takeaways:

    • Use n8n-node-dev init to scaffold
    • Define credentials in separate file
    • Implement execute() with this.helpers.httpRequest
    • Test locally with npm run dev
    • Package with npm pack for distribution

    Need a custom n8n node but don’t want to code? Flowix AI builds custom integrations for businesses. Contact us with your requirements.