Node is designed to perform a specific, reusable task within a workflow.

Initialization

NPX

The npx package for nanoservice-ts is a command-line tool designed to simplify the creation and setup of nanoservice projects, nodes, and more. To learn more, visit NPX.

To create a node, run the following cli command:

1

Run the command

npx nanoctl@latest create node
2

Follow the interactive prompts

 Assign a name to the node: for example `api-call`
3

Install dependencies and start coding

 cd nodes/api-call
 npm install

This will create a new node in the nodes/ directory of the project. To learn how to create a new project, visit Quickstart or NPX documentation.__

Directory Structure

node-template/
│── test/                        # Folder for testing the node
│   └── index.test.ts            # Tests for the custom node
│── config.json                  # JSON schema for node configuration
│── index.ts                     # Main logic of the custom node
│── nodemon.json                 # Config for development using nodemon
│── package.json                 # Project configuration and dependencies
│── README.md                    # Documentation for the custom node
└── tsconfig.json                # TypeScript configuration

Dependencies - package.json

The package.json includes necessary scripts and dependencies for building, testing, and running the node. When working on the core logic of your node in the index.ts file you can install any dependencides of this node there.

{
	"name": "@nanoservice-ts/api-call",
	"version": "0.1.4",
	"description": "Node for making api requrests",
	"author": "Deskree",
	"scripts": {
		"start:dev": "npx nodemon",
		"build": "rimraf ./dist && tsc",
		"build:dev": "tsc --watch"
	},
	"license": "MIT",
	"devDependencies": {
		"@types/node": "^22.13.4",
		"@types/lodash": "^4.14.196",
		"nodemon": "^3.1.9",
		"ts-node": "^10.9.1",
		"typescript": "^5.1.3",
		"rimraf": "^6.0.1"
	},
	"dependencies": {
		"@deskree/blueprint-shared": "0.0.21",
		"@nanoservice-ts/runner": "0.1.14",
		"lodash": "^4.17.21"
	},
	"private": true
}

Creating logic - index.ts

It is the main file where the core logic of the node is written. You can import any installed packagse and use like any other typescript file. However, there are a few rules to follow:

  1. Each node needs to have a handle method.
  2. If a node receives input from the previous node or request body, it should be defined in the inputSchema property. Otherwise, a type of any will be used.
  3. If a node returns a response, it should be defined in the outputSchema property. Otherwise, a type of any will be used.
  4. The handle method should return the output data or a promise that resolves to the output data.
  5. NodeError class should be used to throw errors in the node.

Here is an example of a node structure that has been created via npx nanoctl@latest create node:

import type { BlueprintContext } from "@deskree/blueprint-shared";
import {
	type INanoServiceResponse,
	type JsonLikeObject,
	NanoService,
	NanoServiceResponse,
} from "@nanoservice-ts/runner";

type InputType = {
	// Define the input type here
	message: string;
}

export default class Node extends NanoService<InputType> {
	constructor() {
		super();

		this.inputSchema = {};
		this.outputSchema = {};
	}

	async handle(ctx: BlueprintContext, inputs: InputType): Promise<INanoServiceResponse> {
		const response: NanoServiceResponse = new NanoServiceResponse();

		try {
			response.setSuccess(inputs.message);
		} catch (error: unknown) {
			const nodeError: BlueprintError = new BlueprintError((error as Error).message);
			nodeError.setCode(500);
			nodeError.setStack((error as Error).stack);
			nodeError.setName(this.name);
			response.setError(nodeError); // Set the error
		}

		return response;
	}
}

handle method

Each node needs to have a handle method where all the core logic of the node is written. It accepts two parameters: ctx and inputs and returns a promise of NanoServiceResponse.

async handle(ctx: BlueprintContext, inputs: InputType): Promise<INanoServiceResponse> {
  // Core logic of the node
}
ctx
bojcet

Contains context received by the node from the workflow when it is executed. To learn more about CTX, visit CTX.

inputs
object

Is the data that is being passed to the node when it is executed (from previous nodes) or instantiated (from the static configurations of the workflows)

inputSchema and outputSchema

These properties define the input and output data that is being passed to the node when it is executed. The inputSchema and outputSchema are optional and can be defined as any if not required. Both of them follow JSON Schema format and are in the constructor of the main node class.

The constructor accept config parameter that can be used to pass the configuration to the node in case certain properties need to be defined on build
	export default class NodeExample extends NanoService {
	constructor(config: JsonLikeObject) {
		super();

		// Set the input "JSON Schema Format" here for automated validation
		// Learn JSON Schema: https://json-schema.org/learn/getting-started-step-by-step
		this.inputSchema = {
		};

		// Set the output "JSON Schema Format" here for automated validation
		// Learn JSON Schema: https://json-schema.org/learn/getting-started-step-by-step
		this.outputSchema = {
		};

	}

@nanoservice-ts/runner methods, classes, and interfaces

nanoservice-ts also contains a number of helpers methods and classes that can be used in the node. To learn more, visit Reference.


Configuration - config.json

Defines the JSON Schema for the node configuration. This file defines inputs, outputs, and configurations of each node to ensure that a user passes the required parameters.

At the moment, the config.json file is not used during the node creation and workflows run, but only inside Deskree platfomr. However, it is recommended to define the configuration schema there as it will be used in the near future

Full example

{
	"name": "node-name",
	"description": "",
	"version": "1.0.0",
	"group": "API",
	"config": {
		"type": "object",
		"properties": {
			"inputs": {
				"type": "object",
				"properties": {},
				"required": []
			}
		},
		"required": ["inputs"],
		"example": {
			"inputs": {
				"properties": {}
			}
		}
	},
	"input": {
		"anyOf": [
			{
				"type": "object"
			},
			{
				"type": "array"
			},
			{
				"type": "string"
			}
		],
		"description": "This node accepts an object as input from the previous node or request body"
	},
	"output": {
		"type": "object",
		"description": "The response from the API call"
	},
	"steps": {
		"type": "boolean",
		"default": false
	},
	"functions": {
		"type": "array"
	}
}

Definitions

name
string

The name of the node

description
string

A brief explanation of what the workflow does.

version
string

The version of the workflow, useful for managing updates.

group
string

A category of the node used for internal classification.

config
config object

A JSON Schema that defines the node configuration, which is the data that is being passed to the node when it is instantiated, unlike input that is being passed to the node when it is executed.

input
input object

A JSON Schema that defines the input data that is being passed to the node when it is executed.

output
output object

A JSON Schema that defines the output data that is being returned from the node when it is executed.

steps
steps object

TBD

functions
functions object

TBD


Documenting

It is highly recommended to document the node for better understanding and maintainability. In the near future, we will streamle the process of sharing the nodes with the community, hence the documentation will become a vital part of the project. Currently, the documentation is supported via the following files:

  • README.md
  • CHANGELOG.md

Testing, Debugging & Running

Nodes created via npx command or templates include a test folder and a sample test file (index.test.ts) to validate your node’s logic.

Directory Structure

node-template/
├── test/
│   └── index.test.ts
├── index.ts
├── config.json
└── package.json

Writing Test Cases

Use tools like Jest or Mocha (based on the provided template). The default template includes a basic example to validate your node logic. Example of an index.test.ts file:

import { beforeAll, expect, test } from "vitest";
import Node from "../index";
import ctx from "./helper";

let node: Node;

beforeAll(() => {
	node = new Node();
	node.name = "api-call";
});

// Validate Hello World from Node
test("Hello World from Node", async () => {
	const response = await node.handle(ctx(), {});
	expect({ message: "Hello World from Node!" }, response.data);
});

To run the test, use the following command:

npm run test

IF you need to debug the node, you can run it in the development mode using the following command:

npm run test:dev

This command uses nodemon to restart your node every time you save a change, simplifying the development process. If you want to adjust the process, you access the configuration in the nodemon.json file:

{
  "watch": ["index.ts", "config.json"],
  "ext": "ts,json",
  "exec": "ts-node index.ts"
}

Debugging Techniques

Logging Output

Use console.log() statements to log variables or responses during execution. Example:

console.log("API Response:", result);

Manual Testing

If an error occurs, the BlueprintNode class captures the stack trace and returns a structured error. Check the error logs to locate issues:

try {
// logic
} catch (error) {
	console.error("Error:", error.message);
}

Manual Testing

curl -X POST http://localhost:4000/workflow -H "Content-Type: application/json" -d '{}'

Publishing nodes to npm

Publishing your custom nodes to npm allows other developers to easily install and use them in their workflows.

Follow these steps to publish your custom node to the npm registry:

1

Create an npm Account

If you don’t already have an npm account, sign up at https://www.npmjs.com/signup. Run the following command in your terminal to log in to your npm account:

npm login

Provide your username, password, and email.

2

Update the package.json File

Ensure that your package.json is correctly configured:

Set the name of your node (use a scoped name like @yourusername/node-name for clarity).

Add the appropriate version following semantic versioning.

Provide a description and keywords to make your node discoverable.

Example package.json:

{
	"name": "@yourusername/api-call",
	"version": "1.0.0",
	"description": "A custom node for API calls in nanoservice-ts",
	"main": "index.js",
	"scripts": {
		"build": "tsc",
		"start": "node index.js"
	},
	"keywords": ["nanoservice", "custom-node", "api-call"],
	"license": "MIT",
	"dependencies": {},
	"devDependencies": {
		"typescript": "^5.1.3"
	}
}
3

Build the Node

If your project uses TypeScript, ensure it is built into JavaScript before publishing:

npm run build
4

Publish to npm

Run the following command in your project directory:

npm publish --access public

Use the —access public flag to ensure the package is publicly accessible.

5

Verify Your Node

After publishing:

npm install @yourusername/api-call
6

Keep Your Node Updated

When making changes:

  • Update the version number in package.json (e.g., 1.0.0 -> 1.0.1).
  • Rebuild the project.
  • Run npm publish again to push updates.

Complex Nodes

Complex nodes are nodes that return steps instead of data (ex. control flow nodes)