Rclnodejs 2.0.0 — typed Web SDK for ROS 2, ready for Lyrical

Hi all,

rclnodejs 2.0.0 is out today, on the heels of the ROS 2 Lyrical Luth GA on May 22, 2026. This release adds rclnodejs/web — a typed browser SDK plus a small runtime that lets a web page talk to ROS 2 over a single WebSocket, with the same capabilities also reachable over plain HTTP for curl, Postman, and AI agents.

New in 2.0.0:

  • A typed SDK (rclnodejs/web) for browsers and Node — one ROS 2 type name in, request / reply / message all typed.
  • A capability runtime (npx rclnodejs-web) that only exposes what’s declared in web.json or on the CLI; anything else is rejected with code: 'not_exposed' before any ROS 2 API runs.
  • An HTTP fallback for call and publish — every capability is also reachable over plain POST /capability/<verb>/<name>, so curl, Postman, and AI-agent tool-use work without a hand-written shim.

Carried over from 2.0.0-beta.0 (May 2026): the rosocket WebSocket gateway, ROS 2 Lyrical Luth (Ubuntu 26.04) support, and Linux x64 / arm64 N-API prebuilds — one artifact built against Node.js 20.20.2 runs unchanged on every Node.js ≥ 20.20.2, including 24.x and 26.x — across the full distro matrix (Humble, Jazzy, Kilted, Lyrical, Rolling).

Browser side, in 5 lines

import { connect } from 'rclnodejs/web';

const ros = await connect('ws://robot.local:9000/capability');

const reply = await ros.call<'example_interfaces/srv/AddTwoInts'>(
  '/add_two_ints', { a: '2n', b: '40n' }
);
console.log(reply.sum); // '42n', typed as `${number}n`

Pass a ROS 2 type name as the generic and the corresponding request, reply, or message shape is typed for you.

Server side, no JavaScript needed

source /opt/ros/lyrical/setup.bash
npx -p rclnodejs rclnodejs-web --port 9000 --http-port 9001 \
  --call /add_two_ints=example_interfaces/srv/AddTwoInts
# rclnodejs/web listening on ws://localhost:9000/capability (1 capability)
#                also http://localhost:9001/capability (call/publish only)

Or pass a web.json config file. Either way, the browser only reaches what the server has declared — anything else is rejected with code: 'not_exposed' before any ROS 2 API runs. With --http-port on, every call / publish is also reachable from plain HTTP:

curl -X POST -H 'content-type: application/json' \
  -d '{"a":"2n","b":"40n"}' \
  http://localhost:9001/capability/call/add_two_ints
# => {"sum":"42n"}

Tutorial + demos

Feedback very welcome — particularly on the TypeScript surface, since the wire protocol locks in this release.

For those new to the project: rclnodejs is the Node.js client library for ROS 2, maintained under the Robot Web Tools umbrella.

Cheers,
Minggang

2 Likes

Quick follow-up: rclnodejs@2.1.0-beta.0 is now on npm.

npm i rclnodejs@2.1.0-beta.0

The headline for this one is under the hood: rclnodejs is now a native ES module that ships both ESM and CommonJS from a single package. If you were on 2.0.0, nothing breaks — require('rclnodejs') keeps working — and you can now also import it without a shim.

What’s new in 2.1.0-beta.0:

Dual ESM + CommonJS build. The package is now "type": "module" with a tsup-built dist/ that emits both an ESM (import) and a CommonJS (require) entry, wired through package.json exports conditions. ESM consumers, CommonJS consumers, and TypeScript (NodeNext) all resolve the right artifact automatically — including the rclnodejs/web, rclnodejs/web/server, and rclnodejs/rosocket subpaths.

Top-level await and modern imports. From an ESM project:

import rclnodejs from 'rclnodejs';

await rclnodejs.init();
const node = new rclnodejs.Node('publisher_example_node');
const publisher = node.createPublisher('std_msgs/msg/String', 'topic');
publisher.publish('Hello ROS 2 from rclnodejs');
node.spin();

…or the exact same thing with const rclnodejs = require('rclnodejs') if you’re still on CommonJS.

This is a beta — the dual-format packaging is the main thing to kick the tires on, I’d love to hear your feedback :smiley:

Cheers,
Minggang

2 Likes