In this short guide we will showcase how to instantiate the nanoservice-ts framework and run a simple workflow.

In this example we will create an API that will get the list of all space launches that happened in the year a person was born. The API will accept one parameter dob: string as date of birth and return a json list of space launches.


Initialilizing

Create project

The nanoctl package for nanoservice-ts is a command-line tool designed to simplify the creation and setup of nanoservice projects. It enables developers to quickly initialize ready-to-use project structures with minimal configuration. To learn more about available commands, visit nanoctl

To initialize the project, simply run the following command and follow the instructions of the interactive setup:

1

Run the command

npx nanoctl@latest create project
2

Follow the interactive prompts

 Please provide a name for the project: my-api-project
 Select the trigger to install: HTTP
 Project "my-api-project" created successfully

After creating a project using the npx package, your folder might look like this:

my-api-project/
├── .env.local
├── Dockerfile
├── nodemon.json
├── package.json
├── public
├── workflows/
│   └── example-workflow.json
└── src/
    |── nodes
    |-- types
    ├── Nodes.ts
    ├── Workflow.ts
    └── index.ts

Key Directories:

  • workflows/: Contains your nanoservice workflow definitions.
  • src/: Your project’s main logic and entry point.
  • src/nodes/: Contains custom nodes that extend functionality.

Creating nodes

Node is designed to perform a specific, reusable task within a workflow. To learn more, visit nodes

For this project, we will create 1 custom node:

  • api-call - a node for fetching data from an external API.

Setup

To create your first node, run the following command:

1

Run the command

npx nanoctl@latest create node
2

Follow the interactive prompts

 Assign a name to the node: api-call
3

Install dependencies and start coding

 cd src/nodes/api-call
 npm install

After running the command, a new directory was added to the src/nodes/ folder under the new node’s name with all the necessary files to start coding your custom node:

my-api-project/
├── package.json
├── test/
│   └── example-workflow.json
├── nodes/
│   └── api-call/
│    ├── test/
│    │   └── index.test.ts
│    ├── config.json
│    ├── index.ts
│    ├── nodemon.json
│    ├── package.json
│    ├── README.md
│    └── tsconfig.json
└── src/
    └── index.ts

Defining node logic

The index.ts file is the main file for your custom node. It contains the logic that will be executed by the node during workflow invocation.

In the nodes/api-call/index.ts we will create a node that will fetch data from an external API. For the simplicity of this example it will be hardcoded for only GET requests with headers set to application/json, will receive a URL as an input and return the response.

index.ts
import {
	type INanoServiceResponse,
	type JsonLikeObject,
	NanoService,
	NanoServiceResponse,
} from "@nanoservice-ts/runner";
import { type Context, GlobalError } from "@nanoservice-ts/shared";

export type InputType = {
	method: string;
	url: string;
	headers: JsonLikeObject;
	responseType: string;
	body: JsonLikeObject;
};

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

		this.inputSchema = {
      $schema: "http://json-schema.org/draft-07/schema#",
      title: "Generated schema for Root",
      type: "object",
      properties: {
        url: {
          type: "string",
        },
        method: {
          type: "string",
        },
        body: {
          type: "object",
          properties: {},
        },
        headers: {
          type: "object",
          properties: {},
        },
        responseType: {
          type: "string",
        },
      },
      required: ["url", "method"],
    };

		this.outputSchema = {};
	}

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

		try {
			const method = inputs.method as string;
			const url = inputs.url as string;
			const headers = inputs.headers as JsonLikeObject;
			const responseType = inputs.responseType as string;
			const body = inputs.body || ctx.response.data;

      const options: {
        method: string;
        headers: JsonLikeObject;
        redirect: "follow";
        responseType: string;
        body: string | undefined;
      } = {
        method,
        headers,
        redirect: "follow",
        responseType,
        body: typeof body === "string" ? body : JSON.stringify(body),
      };

      if (method === "GET") options.body = undefined;

			const result: Response = await fetch(url, options as RequestInit);
      if(result.status >= 400 && result.ok === false) {
        throw new Error(result.statusText);
      }

      let parsedResponse: string | JsonLikeObject;
      if (result.headers.get("content-type")?.includes("application/json")) {
        parsedResponse = await result.json();
      } else {
        parsedResponse = await result.text();
      }

			response.setSuccess(parsedResponse);
		} catch (error: unknown) {
			const nodeError: GlobalError = new GlobalError((error as Error).message);
			nodeError.setCode(500);
			nodeError.setStack((error as Error).stack);
			nodeError.setName(this.name);
			response.setError(nodeError);
		}

		return response;
	}
}

Add node configuration (optional)

The config.json file is used to define the expected inputs and outputs for your custom nodes. It provides a structured way to specify the input and output formats, required fields, and other data.
Please note that the config.json file is optional as it is only used by the Deskree platform to connect nodes together. It will be used in the future for other purposes within the framework. Hence, we still recommend creating it as the best practice.

Let’s set up config.json files for both of the nodes we’ve created. For the simplicity of this example, we will define only inputs and outputs, but you can learn more about the advanced configuration options in the Node Configurations.

For the api-call node, the config.json file should look like this:

config.json
{
  "name": "api-call",
  "version": "1.0.0",
  "description": "",
  "group": "API",
  "config": {
		"type": "object",
		"properties": {
			"inputs": {
				"type": "object",
				"properties": {
					"url": {
					  "type": "string"
					},
					"method": {
					  "type": "string"
					},
					"body": {
					  "type": "object",
					  "properties": {}
					},
					"headers": {
					  "type": "object",
					  "properties": {}
					},
					"responseType": {
					  "type": "string"
					}
				  },
				  "required": ["url", "method"]
			}
		},
		"required": ["inputs"],
		"example": {
			"inputs": {
				"properties": {
					"url": "https://jsonplaceholder.typicode.com/posts",
					"method": "GET",
					"headers": {
						"Content-Type": "application/json"
					},
					"body": {}
				}
			}
		}
	},
  "input": {
    "anyOf": [
			{
				"type": "object"
			},
			{
				"type": "array"
			},
			{
				"type": "string"
			}
		],
    "description": "The URL to fetch data from"
  },
  "output": {
    "type": "object",
    "description": "The response from the API call"
  },
  "steps": {
    "type": "boolean",
    "default": false
  },
  "functions": {
    "type": "array"
  }
}

Build the new Node

The nanoservice-ts imports the created nodes at runtime. It is required to build the node using the script build.

To build the node run the following command:

1

Install dependencies and run build

 npm install
 npm run build

After executing the build command a new dist directory will be created in the node’s directory with the compiled files.

Creating a workflow

Workflow is a JSON file that defines the sequence of tasks (nodes) to be executed. Think about it as a set of instructions to the run on how to put your nodes together. To learn more, visit workflows

Now let’s create a workflow that will use the nodes we’ve created. Create a new file in the workflows directory called launches-by-year.json.

launches-by-year.json
{
  "name": "launches-by-year",
  "description": "Get launches by year",
  "trigger": {
		"http": {
			"method": "GET",
			"path": "/",
			"accept": "application/json"
		}
	},
  "steps": [
    {
      "name": "api",
      "node": "api-call",
      "type": "local"
    }
  ],
  "nodes": {
    "api": {
      "inputs": {
        "url": "https://ll.thespacedevs.com/2.3.0/launches/?format=json&year=${ctx.request.query.dob ? new Date(ctx.request.query.dob).getYear() : new Date('2000-01-01').getYear()}",
        "method": "GET",
        "headers": {
          "Content-Type": "application/json"
        },
        "responseType": "json"
      }
    }
  }
}
CTX is a special object that contains the context of the workflow. It is used to pass data between nodes and steps. It is defined on the Trigger level. To learn more, visit CTX

Note that in this example we are using ctx to get the original request query parameter dob and pass it to the get-year node. The get-year node will extract the year from the date of birth and pass it to the final response.

Running Workflow

To test our newly created workflow simply run a command npm run dev in the root directory of the project. This will start the server on the port specified in the .env.local file. By default, you should be able to access at port 4000.

Then make a GET API request to http://localhost:4000/launches-by-year either using a browser or a tool like Postman. The request should contain a query parameter dob with the date of birth in the format YYYY-MM-DD. For example:

curl -X GET http://localhost:4000/launches-by-year?dob=1990-01-01

The response should be a JSON object with the list of space launches that happened in the year 1990.

To learn more about workflow execution, visit Executing Workflows

Summary

You have successfully created a simple API service using nanoservice-ts. You have learned how to create custom nodes, define a workflow, and run it.