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 n8nor terminal output - Use
this.helpers.httpRequestwithrejectUnauthorized: falsefor 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:
- Create GitHub repo (public)
- Add
n8n-nodestopic - Submit via n8n community form: https://docs.n8n.io/integrations/community-nodes/
- 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:
- Build:
npm run build(creates dist/) - Package:
npm pack→ get .tgz file - Install: On production n8n host:
npm install /path/to/package.tgz - Restart n8n:
pm2 restart n8nor docker restart - 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)
- Use
n8n-node-dev initto scaffold - Define credentials in separate file
- Implement
execute()withthis.helpers.httpRequest - Test locally with
npm run dev - Package with
npm packfor distribution
The node will appear automatically in the palette.
Maintenance Tips
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:
Need a custom n8n node but don’t want to code? Flowix AI builds custom integrations for businesses. Contact us with your requirements.