Custom Node Development
Learn how to develop custom n8n nodes for Canvelete and extend the integration with advanced functionality.
Overview
While the official Canvelete n8n community node covers most use cases, you may need to create custom nodes for:
- Specialized Operations: Custom business logic or data transformations
- Advanced Authentication: Custom OAuth2 flows or API key management
- Extended Functionality: Features not available in the community node
- Enterprise Integration: Custom endpoints or proprietary workflows
This guide covers the complete process of developing, testing, and deploying custom n8n nodes for Canvelete.
Prerequisites
Before developing custom nodes, ensure you have:
- Node.js: Version 18 or higher
- TypeScript: Familiarity with TypeScript development
- n8n Knowledge: Understanding of n8n's architecture and node system
- Canvelete API: Knowledge of Canvelete's REST API endpoints
- Development Environment: Local n8n installation for testing
Development Environment Setup
1. Initialize Node Project
Create a new directory for your custom node:
# Create project directory
mkdir n8n-nodes-canvelete-custom
cd n8n-nodes-canvelete-custom
# Initialize npm project
npm init -y
# Install n8n development dependencies
npm install --save-dev n8n-workflow n8n-core @types/node typescript
npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm install --save-dev eslint prettier
# Install runtime dependencies
npm install axios form-data2. Configure TypeScript
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"lib": ["ES2019"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}3. Project Structure
Create the following directory structure:
n8n-nodes-canvelete-custom/
├── src/
│ ├── credentials/
│ │ └── CanveleteCustomApi.credentials.ts
│ ├── nodes/
│ │ └── CanveleteCustom/
│ │ ├── CanveleteCustom.node.ts
│ │ ├── GenericFunctions.ts
│ │ ├── descriptions/
│ │ │ ├── DesignDescription.ts
│ │ │ ├── RenderDescription.ts
│ │ │ └── TemplateDescription.ts
│ │ └── canvelete-custom.svg
│ └── types/
│ └── index.ts
├── package.json
├── tsconfig.json
└── README.mdCreating Custom Credentials
OAuth2 Credential Implementation
Create src/credentials/CanveleteCustomApi.credentials.ts:
import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class CanveleteCustomApi implements ICredentialType {
name = 'canveleteCustomApi';
displayName = 'Canvelete Custom API';
documentationUrl = 'https://docs.canvelete.com/integrations/n8n/custom-nodes';
properties: INodeProperties[] = [
{
displayName: 'Grant Type',
name: 'grantType',
type: 'hidden',
default: 'authorizationCode',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden',
default: 'https://canvelete.com/api/oauth/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: 'https://canvelete.com/api/oauth/token',
},
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
default: 'n8n-custom',
required: true,
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string',
typeOptions: {
password: true,
},
default: '',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'openid profile email designs:read designs:write templates:read render:write apikeys:read apikeys:write',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden',
default: 'body',
},
{
displayName: 'Environment',
name: 'environment',
type: 'options',
options: [
{
name: 'Production',
value: 'production',
},
{
name: 'Beta',
value: 'beta',
},
{
name: 'Development',
value: 'development',
},
],
default: 'beta',
description: 'Canvelete environment to connect to',
},
{
displayName: 'Custom Base URL',
name: 'customBaseUrl',
type: 'string',
displayOptions: {
show: {
environment: ['development'],
},
},
default: 'http://localhost:3000',
placeholder: 'http://localhost:3000',
description: 'Custom base URL for development environment',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.oauthTokenData.access_token}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: '={{$credentials.environment === "production" ? "https://canvelete.com" : $credentials.environment === "beta" ? "https://canvelete.com" : $credentials.customBaseUrl}}',
url: '/api/automation/designs',
method: 'GET',
qs: {
limit: 1,
},
},
};
}API Key Credential (Alternative)
Create src/credentials/CanveleteApiKey.credentials.ts:
import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class CanveleteApiKey implements ICredentialType {
name = 'canveleteApiKey';
displayName = 'Canvelete API Key';
documentationUrl = 'https://docs.canvelete.com/integrations/n8n/custom-nodes';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: {
password: true,
},
default: '',
required: true,
description: 'Your Canvelete API key',
},
{
displayName: 'Environment',
name: 'environment',
type: 'options',
options: [
{
name: 'Production',
value: 'production',
},
{
name: 'Beta',
value: 'beta',
},
{
name: 'Development',
value: 'development',
},
],
default: 'beta',
},
{
displayName: 'Custom Base URL',
name: 'customBaseUrl',
type: 'string',
displayOptions: {
show: {
environment: ['development'],
},
},
default: 'http://localhost:3000',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
'X-API-Key': '={{$credentials.apiKey}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: '={{$credentials.environment === "production" ? "https://canvelete.com" : $credentials.environment === "beta" ? "https://canvelete.com" : $credentials.customBaseUrl}}',
url: '/api/automation/designs',
method: 'GET',
},
};
}Building Custom Node Logic
Generic Functions
Create src/nodes/CanveleteCustom/GenericFunctions.ts:
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
IHttpRequestOptions,
IDataObject,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
export async function canveleteApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
method: string,
resource: string,
body: any = {},
qs: IDataObject = {},
uri?: string,
headers: IDataObject = {},
): Promise<any> {
const credentials = await this.getCredentials('canveleteCustomApi');
const baseUrl = getBaseUrl(credentials);
const options: IHttpRequestOptions = {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
body,
qs,
uri: uri || `${baseUrl}/api/automation${resource}`,
json: true,
};
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
return await this.helpers.httpRequestWithAuthentication.call(
this,
'canveleteCustomApi',
options,
);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export async function canveleteApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: string,
endpoint: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
query.limit = 100;
query.offset = 0;
do {
responseData = await canveleteApiRequest.call(this, method, endpoint, body, query);
if (responseData[propertyName]) {
returnData.push.apply(returnData, responseData[propertyName]);
}
query.offset = (query.offset as number) + (query.limit as number);
} while (responseData[propertyName] && responseData[propertyName].length !== 0);
return returnData;
}
export function getBaseUrl(credentials: IDataObject): string {
const environment = credentials.environment as string;
switch (environment) {
case 'production':
return 'https://canvelete.com';
case 'beta':
return 'https://canvelete.com';
case 'development':
return credentials.customBaseUrl as string;
default:
return 'https://canvelete.com';
}
}
export function validateDynamicData(data: string): object {
try {
return JSON.parse(data);
} catch (error) {
throw new NodeOperationError(
{ type: 'n8n-nodes-canvelete-custom', typeVersion: 1 } as any,
`Invalid JSON in dynamic data: ${error.message}`,
);
}
}
export function formatRenderOptions(options: IDataObject): IDataObject {
const formatted: IDataObject = {};
if (options.format) {
formatted.format = options.format;
}
if (options.width) {
formatted.width = parseInt(options.width as string, 10);
}
if (options.height) {
formatted.height = parseInt(options.height as string, 10);
}
if (options.quality) {
formatted.quality = parseInt(options.quality as string, 10);
}
if (options.dynamicData) {
formatted.dynamicData = validateDynamicData(options.dynamicData as string);
}
return formatted;
}Main Node Implementation
Create src/nodes/CanveleteCustom/CanveleteCustom.node.ts:
import {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
canveleteApiRequest,
canveleteApiRequestAllItems,
formatRenderOptions,
} from './GenericFunctions';
import { designOperations, designFields } from './descriptions/DesignDescription';
import { renderOperations, renderFields } from './descriptions/RenderDescription';
import { templateOperations, templateFields } from './descriptions/TemplateDescription';
export class CanveleteCustom implements INodeType {
description: INodeTypeDescription = {
displayName: 'Canvelete Custom',
name: 'canveleteCustom',
icon: 'file:canvelete-custom.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Custom Canvelete node with advanced functionality',
defaults: {
name: 'Canvelete Custom',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'canveleteCustomApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Design',
value: 'design',
},
{
name: 'Render',
value: 'render',
},
{
name: 'Template',
value: 'template',
},
{
name: 'Batch',
value: 'batch',
},
{
name: 'Analytics',
value: 'analytics',
},
],
default: 'design',
},
...designOperations,
...renderOperations,
...templateOperations,
// Batch operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['batch'],
},
},
options: [
{
name: 'Render Multiple',
value: 'renderMultiple',
description: 'Render multiple templates with different data',
action: 'Render multiple templates',
},
{
name: 'Create Designs Batch',
value: 'createDesignsBatch',
description: 'Create multiple designs in batch',
action: 'Create multiple designs',
},
],
default: 'renderMultiple',
},
// Analytics operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['analytics'],
},
},
options: [
{
name: 'Get Usage Stats',
value: 'getUsageStats',
description: 'Get API usage statistics',
action: 'Get usage statistics',
},
{
name: 'Get Render History',
value: 'getRenderHistory',
description: 'Get detailed render history with analytics',
action: 'Get render history',
},
],
default: 'getUsageStats',
},
...designFields,
...renderFields,
...templateFields,
// Batch fields
{
displayName: 'Batch Data',
name: 'batchData',
type: 'json',
displayOptions: {
show: {
resource: ['batch'],
operation: ['renderMultiple', 'createDesignsBatch'],
},
},
default: '[]',
description: 'Array of objects containing batch operation data',
placeholder: '[{"templateId": "123", "dynamicData": {"title": "Item 1"}}, {"templateId": "456", "dynamicData": {"title": "Item 2"}}]',
},
// Analytics fields
{
displayName: 'Date Range',
name: 'dateRange',
type: 'options',
displayOptions: {
show: {
resource: ['analytics'],
},
},
options: [
{
name: 'Last 7 Days',
value: '7d',
},
{
name: 'Last 30 Days',
value: '30d',
},
{
name: 'Last 90 Days',
value: '90d',
},
{
name: 'Custom Range',
value: 'custom',
},
],
default: '7d',
},
{
displayName: 'Start Date',
name: 'startDate',
type: 'dateTime',
displayOptions: {
show: {
resource: ['analytics'],
dateRange: ['custom'],
},
},
default: '',
description: 'Start date for custom range',
},
{
displayName: 'End Date',
name: 'endDate',
type: 'dateTime',
displayOptions: {
show: {
resource: ['analytics'],
dateRange: ['custom'],
},
},
default: '',
description: 'End date for custom range',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
try {
let responseData;
if (resource === 'design') {
responseData = await this.executeDesignOperation(i, operation);
} else if (resource === 'render') {
responseData = await this.executeRenderOperation(i, operation);
} else if (resource === 'template') {
responseData = await this.executeTemplateOperation(i, operation);
} else if (resource === 'batch') {
responseData = await this.executeBatchOperation(i, operation);
} else if (resource === 'analytics') {
responseData = await this.executeAnalyticsOperation(i, operation);
} else {
throw new NodeOperationError(
this.getNode(),
`The resource "${resource}" is not known!`,
{ itemIndex: i },
);
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData);
} else {
returnData.push({
json: responseData,
pairedItem: { item: i },
});
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: { error: error.message },
pairedItem: { item: i },
});
continue;
}
throw error;
}
}
return [returnData];
}
private async executeDesignOperation(itemIndex: number, operation: string): Promise<any> {
// Implementation for design operations
// Similar to community node but with custom enhancements
if (operation === 'create') {
const name = this.getNodeParameter('name', itemIndex) as string;
const description = this.getNodeParameter('description', itemIndex, '') as string;
const width = this.getNodeParameter('width', itemIndex, 1920) as number;
const height = this.getNodeParameter('height', itemIndex, 1080) as number;
const canvasData = this.getNodeParameter('canvasData', itemIndex, '{}') as string;
const body = {
name,
description,
width,
height,
canvasData: JSON.parse(canvasData),
};
return await canveleteApiRequest.call(this, 'POST', '/designs', body);
}
// Add other design operations...
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported for resource "design"!`);
}
private async executeRenderOperation(itemIndex: number, operation: string): Promise<any> {
// Implementation for render operations with custom enhancements
if (operation === 'create') {
const templateId = this.getNodeParameter('templateId', itemIndex, '') as string;
const designId = this.getNodeParameter('designId', itemIndex, '') as string;
if (!templateId && !designId) {
throw new NodeOperationError(this.getNode(), 'Either Template ID or Design ID must be provided!');
}
const options = formatRenderOptions({
format: this.getNodeParameter('format', itemIndex, 'png'),
width: this.getNodeParameter('width', itemIndex, ''),
height: this.getNodeParameter('height', itemIndex, ''),
quality: this.getNodeParameter('quality', itemIndex, ''),
dynamicData: this.getNodeParameter('dynamicData', itemIndex, ''),
});
const body: any = { ...options };
if (templateId) {
body.templateId = templateId;
} else {
body.designId = designId;
}
return await canveleteApiRequest.call(this, 'POST', '/render', body);
}
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported for resource "render"!`);
}
private async executeTemplateOperation(itemIndex: number, operation: string): Promise<any> {
// Implementation for template operations
if (operation === 'getMany') {
const limit = this.getNodeParameter('limit', itemIndex, 50) as number;
const returnAll = this.getNodeParameter('returnAll', itemIndex, false) as boolean;
if (returnAll) {
return await canveleteApiRequestAllItems.call(this, 'templates', 'GET', '/templates');
} else {
const qs = { limit };
return await canveleteApiRequest.call(this, 'GET', '/templates', {}, qs);
}
}
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported for resource "template"!`);
}
private async executeBatchOperation(itemIndex: number, operation: string): Promise<any> {
// Custom batch operations
if (operation === 'renderMultiple') {
const batchData = JSON.parse(this.getNodeParameter('batchData', itemIndex, '[]') as string);
const results = [];
for (const item of batchData) {
try {
const result = await canveleteApiRequest.call(this, 'POST', '/render', item);
results.push({ success: true, data: result, input: item });
} catch (error) {
results.push({ success: false, error: error.message, input: item });
}
}
return results.map(result => ({ json: result }));
}
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported for resource "batch"!`);
}
private async executeAnalyticsOperation(itemIndex: number, operation: string): Promise<any> {
// Custom analytics operations
if (operation === 'getUsageStats') {
const dateRange = this.getNodeParameter('dateRange', itemIndex) as string;
let qs: any = {};
if (dateRange === 'custom') {
qs.startDate = this.getNodeParameter('startDate', itemIndex) as string;
qs.endDate = this.getNodeParameter('endDate', itemIndex) as string;
} else {
qs.range = dateRange;
}
return await canveleteApiRequest.call(this, 'GET', '/analytics/usage', {}, qs);
}
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported for resource "analytics"!`);
}
}Advanced Features
Webhook Support
Create webhook functionality for real-time events:
// In your node class
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId === undefined) {
return false;
}
const endpoint = `/webhooks/${webhookData.webhookId}`;
try {
await canveleteApiRequest.call(this, 'GET', endpoint);
return true;
} catch (error) {
return false;
}
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
const events = this.getNodeParameter('events') as string[];
const body = {
url: webhookUrl,
events,
active: true,
};
const responseData = await canveleteApiRequest.call(this, 'POST', '/webhooks', body);
if (responseData.id === undefined) {
return false;
}
webhookData.webhookId = responseData.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/webhooks/${webhookData.webhookId}`;
try {
await canveleteApiRequest.call(this, 'DELETE', endpoint);
} catch (error) {
return false;
}
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
return {
workflowData: [
this.helpers.returnJsonArray(bodyData),
],
};
}Load Options for Dynamic Dropdowns
Add dynamic loading for templates and designs:
methods = {
loadOptions: {
async getTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
try {
const templates = await canveleteApiRequest.call(this, 'GET', '/templates', {}, { limit: 100 });
for (const template of templates.templates) {
returnData.push({
name: template.name,
value: template.id,
description: template.description,
});
}
return returnData;
} catch (error) {
throw new NodeOperationError(this.getNode(), `Failed to load templates: ${error.message}`);
}
},
async getDesigns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
try {
const designs = await canveleteApiRequest.call(this, 'GET', '/designs', {}, { limit: 100 });
for (const design of designs.designs) {
returnData.push({
name: design.name,
value: design.id,
description: design.description || 'No description',
});
}
return returnData;
} catch (error) {
throw new NodeOperationError(this.getNode(), `Failed to load designs: ${error.message}`);
}
},
},
};Testing and Debugging
Unit Testing
Create src/test/CanveleteCustom.node.test.ts:
import { IExecuteFunctions } from 'n8n-workflow';
import { CanveleteCustom } from '../nodes/CanveleteCustom/CanveleteCustom.node';
describe('CanveleteCustom', () => {
let node: CanveleteCustom;
let mockExecuteFunctions: Partial<IExecuteFunctions>;
beforeEach(() => {
node = new CanveleteCustom();
mockExecuteFunctions = {
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
getNodeParameter: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({
environment: 'beta',
clientId: 'test-client',
clientSecret: 'test-secret',
}),
helpers: {
httpRequestWithAuthentication: jest.fn(),
},
};
});
describe('Design Operations', () => {
it('should create a design successfully', async () => {
mockExecuteFunctions.getNodeParameter = jest.fn()
.mockReturnValueOnce('design') // resource
.mockReturnValueOnce('create') // operation
.mockReturnValueOnce('Test Design') // name
.mockReturnValueOnce('Test Description') // description
.mockReturnValueOnce(1920) // width
.mockReturnValueOnce(1080); // height
mockExecuteFunctions.helpers!.httpRequestWithAuthentication = jest.fn()
.mockResolvedValue({
id: 'design_123',
name: 'Test Design',
description: 'Test Description',
});
const result = await node.execute.call(mockExecuteFunctions as IExecuteFunctions);
expect(result[0]).toHaveLength(1);
expect(result[0][0].json).toEqual({
id: 'design_123',
name: 'Test Design',
description: 'Test Description',
});
});
});
describe('Batch Operations', () => {
it('should handle batch rendering', async () => {
const batchData = [
{ templateId: 'template_1', dynamicData: { title: 'Item 1' } },
{ templateId: 'template_2', dynamicData: { title: 'Item 2' } },
];
mockExecuteFunctions.getNodeParameter = jest.fn()
.mockReturnValueOnce('batch') // resource
.mockReturnValueOnce('renderMultiple') // operation
.mockReturnValueOnce(JSON.stringify(batchData)); // batchData
mockExecuteFunctions.helpers!.httpRequestWithAuthentication = jest.fn()
.mockResolvedValueOnce({ id: 'render_1', url: 'https://example.com/1.png' })
.mockResolvedValueOnce({ id: 'render_2', url: 'https://example.com/2.png' });
const result = await node.execute.call(mockExecuteFunctions as IExecuteFunctions);
expect(result[0]).toHaveLength(2);
expect(result[0][0].json.success).toBe(true);
expect(result[0][1].json.success).toBe(true);
});
});
});Integration Testing
Create src/test/integration.test.ts:
import { CanveleteCustom } from '../nodes/CanveleteCustom/CanveleteCustom.node';
describe('Integration Tests', () => {
let node: CanveleteCustom;
beforeEach(() => {
node = new CanveleteCustom();
});
it('should connect to Canvelete API', async () => {
// Test with real credentials in CI/CD environment
if (process.env.CANVELETE_TEST_API_KEY) {
// Run integration tests
} else {
console.log('Skipping integration tests - no API key provided');
}
});
});Building and Packaging
Build Configuration
Update package.json:
{
"name": "n8n-nodes-canvelete-custom",
"version": "1.0.0",
"description": "Custom n8n node for Canvelete with advanced features",
"keywords": ["n8n-community-node-package"],
"license": "MIT",
"homepage": "https://docs.canvelete.com/integrations/n8n/custom-nodes",
"author": {
"name": "Your Name",
"email": "your.email@example.com"
},
"repository": {
"type": "git",
"url": "https://github.com/your-org/n8n-nodes-canvelete-custom.git"
},
"main": "dist/index.js",
"scripts": {
"build": "tsc && npm run copy-assets",
"copy-assets": "cp src/nodes/CanveleteCustom/*.svg dist/nodes/CanveleteCustom/",
"dev": "tsc --watch",
"format": "prettier --write .",
"lint": "eslint . --ext .ts",
"test": "jest",
"test:watch": "jest --watch",
"prepublishOnly": "npm run build && npm run lint && npm run test"
},
"files": [
"dist"
],
"n8n": {
"n8nNodesApiVersion": 1,
"credentials": [
"dist/credentials/CanveleteCustomApi.credentials.js",
"dist/credentials/CanveleteApiKey.credentials.js"
],
"nodes": [
"dist/nodes/CanveleteCustom/CanveleteCustom.node.js"
]
},
"devDependencies": {
"@types/jest": "^29.5.0",
"@types/node": "^18.15.0",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"eslint": "^8.37.0",
"jest": "^29.5.0",
"n8n-core": "^0.220.0",
"n8n-workflow": "^0.220.0",
"prettier": "^2.8.7",
"ts-jest": "^29.1.0",
"typescript": "^5.0.2"
},
"dependencies": {
"axios": "^1.3.4",
"form-data": "^4.0.0"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"testMatch": ["**/*.test.ts"]
}
}Build Process
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
# Lint code
npm run lint
# Format code
npm run formatDeployment and Distribution
Local Installation
# Build the node
npm run build
# Link for local development
npm link
# In n8n directory
cd ~/.n8n
npm link n8n-nodes-canvelete-custom
# Restart n8n
n8n startNPM Publishing
# Login to npm
npm login
# Publish to npm registry
npm publish
# Or publish with specific tag
npm publish --tag betaPrivate Registry
For enterprise deployments:
# Configure private registry
npm config set registry https://your-private-registry.com
# Publish to private registry
npm publishCommunity Guidelines
Contributing to the Official Node
If you develop useful features, consider contributing back:
-
Fork the Repository
git clone https://github.com/amanuel-1/canvelete.git cd canvelete/n8n-nodes-canvelete -
Create Feature Branch
git checkout -b feature/your-feature-name -
Implement Changes
- Follow existing code patterns
- Add comprehensive tests
- Update documentation
-
Submit Pull Request
- Describe the feature and use case
- Include tests and documentation
- Follow the contribution guidelines
Sharing Custom Nodes
Share your custom nodes with the community:
-
Documentation
- Create comprehensive README
- Include usage examples
- Document all parameters
-
Testing
- Provide unit tests
- Include integration tests
- Test with different n8n versions
-
Community Channels
- Share in n8n community forum
- Post in Canvelete community
- Create GitHub repository
Best Practices
-
Code Quality
- Follow TypeScript best practices
- Use consistent naming conventions
- Implement proper error handling
-
User Experience
- Provide clear parameter descriptions
- Use appropriate input validation
- Include helpful error messages
-
Performance
- Implement efficient API calls
- Use appropriate caching strategies
- Handle rate limiting gracefully
-
Security
- Validate all inputs
- Use secure credential storage
- Follow OAuth2 best practices
Support and Resources
Documentation
- n8n Node Development Guide (opens in a new tab)
- Canvelete API Reference
- TypeScript Documentation (opens in a new tab)
Community
- n8n Community Forum (opens in a new tab)
- Canvelete Community (opens in a new tab)
- GitHub Discussions (opens in a new tab)
Support
- Email: support@canvelete.com
- Response Time: 24-48 hours for development questions
- Include: Node version, n8n version, error logs, code samples
Troubleshooting
Common Issues
Node Not Loading
# Clear n8n cache
rm -rf ~/.n8n/cache
# Restart n8n
n8n startTypeScript Compilation Errors
# Check TypeScript version compatibility
npm ls typescript
# Update dependencies
npm updateAuthentication Issues
# Test credentials manually
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://canvelete.com/api/automation/designsPerformance Issues
- Implement request batching
- Add appropriate delays
- Use connection pooling
- Monitor memory usage
This comprehensive guide provides everything needed to develop, test, and deploy custom n8n nodes for Canvelete, enabling advanced automation scenarios and enterprise integrations.