↓ Skip to main content
  1. Blog/

Lessons Learned: Publishing 7 npm Packages for Industrial IoT

·14 mins
Table of Contents
IIoT - This article is part of a series.
Part 14: This Article

Update (July 2026): the family has since grown to eight packages — node-red-contrib-kafka-suite joined after this post was written. Everything below still applies.

Since November 2025 I’ve published seven npm packages for Node-RED, all targeting the IIoT space. Some took off, some needed multiple rewrites, and all taught me things I couldn’t have learned from tutorials alone. This post is the honest rundown: what worked, what didn’t, and what I’d do differently.


The Seven Packages
#

Test counts and downloads below are the current state of each repository and the npm registry, not the figures from the day I first published.

PackagePurposeTestsDownloads
node-red-contrib-condition-monitoringVibration analysis, FFT, alarm thresholds4182,350+
node-red-contrib-s7-suiteSiemens S7 PLC communication4412,950+
node-red-contrib-opcua-suiteOPC-UA client with browse, read, write, subscribe6293,400+
node-red-contrib-cip-suiteAllen-Bradley PLC & EtherNet/IP communication991,880+
node-red-contrib-i3xCESMII i3x open manufacturing API1191,530+
node-red-contrib-clab-interfacesCompuLab IoT Gateway hardware interfaces64570+
node-red-contrib-nats-suiteNATS messaging (pub/sub, request/reply, JetStream)41,930+

That last row is the honest one: nats-suite has the fewest tests and the third-most downloads. It is the package I would least like to refactor.

Each one started the same way: I needed something for a project, couldn’t find a good existing solution, and built it myself.


Architecture Decisions
#

Monorepo vs Separate Repos
#

I started with separate repos for each package. After the third package, I considered a monorepo but decided against it. Here’s why:

Monorepo (Lerna / npm workspaces)

packages/
├── condition-monitoring/
├── cip-suite/
├── s7-suite/
├── nats-suite/
├── opcua-suite/
├── clab-interfaces/
└── i3x/
  • ✓ Shared dev dependencies
  • ✓ Cross-package changes atomic
  • ✗ Complex CI/CD
  • ✗ Intimidating for contributors
  • ✗ Version coupling risk

Separate repos

PackageVersion
condition-monitoringv0.3.1
nats-suitev0.2.2
opcua-suitev0.1.7
cip-suitev0.0.5
s7-suitev0.0.7
i3xv0.0.8
clab-interfacesv0.0.1
  • ✓ Simple CI/CD per package
  • ✓ Independent versioning
  • ✓ Lower contributor barrier
  • ✗ Duplicated boilerplate
  • ✗ Shared fixes need N commits

My recommendation: Use separate repos unless your packages genuinely share significant code. IIoT packages rarely do — a NATS client has nothing in common with a vibration analysis module.

When to Split a Node into Multiple Nodes
#

My first version of opcua-suite had a single “OPC-UA” node that tried to do everything: browse, read, write, subscribe, monitor. It was a mess — the configuration panel had 30 fields.

I split it into focused nodes:

After (v0.3) — 5–8 config fields each, clear purpose

opcua-connection
(config node)

opcua-browse

opcua-read

opcua-write

opcua-subscribe

opcua-method-call

Before (v0.1)

opcua-all-in-one
Browse? Read? Write?
Subscribe? Monitor?
30 config fields
Confused users

After (v0.3) — 5–8 config fields each, clear purpose

opcua-connection
(config node)

opcua-browse

opcua-read

opcua-write

opcua-subscribe

opcua-method-call

Before (v0.1)

opcua-all-in-one
Browse? Read? Write?
Subscribe? Monitor?
30 config fields
Confused users

Rule of thumb: If a node needs a dropdown to select its mode of operation, split it. Each node should do one thing well.

Config Nodes for Connections
#

Every package that connects to an external service (NATS broker, OPC-UA server, i3x API) uses a config node for the connection. The config node manages the connection lifecycle, and operation nodes reference it:

Node-RED Flow — 3 operation nodes, 1 shared connection

nats-pub

nats-conn (config)
host: ...
port: ...
creds: ...

nats-sub

nats-req

Node-RED Flow — 3 operation nodes, 1 shared connection

nats-pub

nats-conn (config)
host: ...
port: ...
creds: ...

nats-sub

nats-req

This pattern prevents connection sprawl (ten nodes opening ten separate TCP connections) and centralizes credential management.


Testing
#

node-red-node-test-helper
#

The node-red-node-test-helper package is the official way to unit test Node-RED nodes. It creates a lightweight Node-RED runtime in memory, loads your nodes, and lets you send messages and check outputs.

Testing Industrial Protocols is Hard
#

The challenge with IIoT nodes: they interact with real hardware and services. You can’t run an OPC-UA server in CI (well, you can, but it’s brittle). You can’t plug in a CAN bus adapter on GitHub Actions.

My approach: three testing layers.

Layer 1: Pure Logic Tests (fast)
• Data parsing, encoding, validation
• Math (FFT, RMS, thresholds)
• No external dependencies
• 80% of tests live here

Layer 2: Node Behavior Tests (medium)
• node-red-node-test-helper
• Mocked connections
• Input/output message validation
• Status and error handling

Layer 3: Integration Tests (slow)
• Docker Compose with real services
• NATS server, OPC-UA simulator
• End-to-end message flow
• Run manually or nightly in CI

Layer 1: Pure Logic Tests (fast)
• Data parsing, encoding, validation
• Math (FFT, RMS, thresholds)
• No external dependencies
• 80% of tests live here

Layer 2: Node Behavior Tests (medium)
• node-red-node-test-helper
• Mocked connections
• Input/output message validation
• Status and error handling

Layer 3: Integration Tests (slow)
• Docker Compose with real services
• NATS server, OPC-UA simulator
• End-to-end message flow
• Run manually or nightly in CI

Example: Testing the Condition Monitoring FFT Node
#

The condition monitoring package does real signal processing — Fast Fourier Transform, RMS calculation, threshold-based alarms. The math must be correct.

// test/signal-analyzer_spec.js
const { expect } = require("chai");
const { computeFFT, findPeaks, calculateRMS } = require("../lib/signal");

describe('Signal Processing', function() {
    describe('FFT', function() {
        it('should detect a 50Hz component in a 1kHz sampled signal', function() {
            const sampleRate = 1000;
            const duration = 1;
            const samples = [];

            for (let i = 0; i < sampleRate * duration; i++) {
                const t = i / sampleRate;
                samples.push(Math.sin(2 * Math.PI * 50 * t));
            }

            const result = computeFFT(samples, sampleRate);
            const peaks = findPeaks(result.magnitudes, result.frequencies, {
                threshold: 0.1
            });

            expect(peaks).to.have.length(1);
            expect(peaks[0].frequency).to.be.closeTo(50, 1);
        });

        it('should detect multiple frequency components', function() {
            const sampleRate = 1000;
            const samples = [];

            for (let i = 0; i < sampleRate; i++) {
                const t = i / sampleRate;
                samples.push(
                    1.0 * Math.sin(2 * Math.PI * 25 * t) +
                    0.5 * Math.sin(2 * Math.PI * 75 * t) +
                    0.3 * Math.sin(2 * Math.PI * 150 * t)
                );
            }

            const result = computeFFT(samples, sampleRate);
            const peaks = findPeaks(result.magnitudes, result.frequencies, {
                threshold: 0.05
            });

            expect(peaks).to.have.length(3);
            expect(peaks[0].frequency).to.be.closeTo(25, 1);
            expect(peaks[1].frequency).to.be.closeTo(75, 1);
            expect(peaks[2].frequency).to.be.closeTo(150, 1);
        });
    });

    describe('RMS', function() {
        it('should calculate RMS of a sine wave', function() {
            const samples = [];
            for (let i = 0; i < 1000; i++) {
                samples.push(Math.sin(2 * Math.PI * i / 1000));
            }
            const rms = calculateRMS(samples);
            expect(rms).to.be.closeTo(1 / Math.sqrt(2), 0.01);
        });

        it('should return 0 for all-zero input', function() {
            const rms = calculateRMS(new Array(100).fill(0));
            expect(rms).to.equal(0);
        });
    });
});

Mocking Industrial Protocols
#

For node behavior tests, I mock the protocol clients:

// test/nats-publish_spec.js
const helper = require("node-red-node-test-helper");
const sinon = require("sinon");
const natsNode = require("../nodes/nats-publish");
const natsConnNode = require("../nodes/nats-connection");

helper.init(require.resolve('node-red'));

describe('nats-publish Node', function() {
    let connectStub;

    beforeEach(function() {
        connectStub = sinon.stub().resolves({
            publish: sinon.stub(),
            drain: sinon.stub().resolves(),
            close: sinon.stub().resolves(),
            status: sinon.stub().returns({ data: 'connected' }),
        });
    });

    afterEach(function() {
        helper.unload();
        sinon.restore();
    });

    it('should publish a message to the configured subject', function(done) {
        const flow = [
            {
                id: "conn1",
                type: "nats-connection",
                name: "Test NATS",
                server: "nats://localhost:4222"
            },
            {
                id: "n1",
                type: "nats-publish",
                name: "Publish",
                connection: "conn1",
                subject: "factory.line1.status",
                wires: []
            }
        ];

        helper.load([natsConnNode, natsNode], flow, function() {
            const n1 = helper.getNode("n1");
            const conn = helper.getNode("conn1");

            conn.client = connectStub();

            n1.receive({ payload: { status: "running" } });

            setTimeout(function() {
                try {
                    expect(conn.client.publish.calledOnce).to.be.true;
                    done();
                } catch(err) {
                    done(err);
                }
            }, 100);
        });
    });
});

Test Count: 418 Tests in Condition Monitoring
#

The condition monitoring package has the most tests because it does the most math. Here’s the breakdown as of 0.3.1:

Test fileTestsCovers
llm-analyzer_spec.js79Prompting, parsing, failure modes
websocket-manager_spec.js26Live streaming, reconnects
signal-analyzer_spec.js25FFT, RMS, envelope, cepstrum
training-data-collector_spec.js24Labelled export, CSV/JSONL
max-bridge-manager_spec.js24Bridge lifecycle
state-persistence_spec.js23Surviving a restart
anomaly-detector_spec.js21Z-score, IQR, CUSUM, hysteresis
pca-anomaly_spec.js20Multivariate detection
16 further unit specs145Nodes, sources, utilities
test/integration/*31End-to-end flows in a real runtime
Total418

Running them:

$ npm test

  Signal Processing
    FFT
      ✓ should detect a 50Hz component (12ms)
      ✓ should detect multiple frequency components (15ms)
      ✓ should handle power-of-2 sample sizes (8ms)
      ✓ should zero-pad non-power-of-2 inputs (11ms)
      ...

  Threshold Alarms
    ✓ should trigger WARNING when value exceeds warning threshold
    ✓ should trigger CRITICAL when value exceeds critical threshold
    ✓ should clear alarm when value returns below threshold
    ✓ should support hysteresis to prevent alarm flapping
    ...

  418 passing (6.1s)

Documentation
#

README as Marketing
#

Your README is the first thing anyone sees. For IIoT packages, the README needs to answer three questions immediately:

  1. What does this do? (one sentence)
  2. How do I install it? (one command)
  3. What does it look like? (screenshot or example)
# node-red-contrib-condition-monitoring

Vibration analysis and condition monitoring nodes for Node-RED.
FFT, RMS, envelope analysis, and threshold-based alarms — 
built for predictive maintenance on industrial machines.

## Install

\`\`\`bash
cd ~/.node-red
npm install node-red-contrib-condition-monitoring
\`\`\`

## Nodes

| Node | Purpose |
|------|---------|
| **fft-analysis** | Compute FFT spectrum from time-domain samples |
| **rms-monitor** | Calculate running RMS with configurable window |
| **threshold-alarm** | Trigger alarms based on configurable thresholds |
| **envelope-analysis** | Bearing fault detection via envelope analysis |
| **trend-detect** | Detect upward/downward trends over time |

## Example Flow

[Screenshot here]
[Importable flow JSON here]

Example Flows
#

Every package includes at least one importable example flow. Node-RED has a built-in example import feature — if you put example flows in the right directory, users can import them from Import → Examples → your-package-name:

your-package/
├── examples/
│   ├── basic-fft-analysis.json
│   ├── multi-axis-monitoring.json
│   └── mqtt-to-alarm-pipeline.json
├── nodes/
│   └── ...
└── package.json

In package.json:

{
  "node-red": {
    "version": ">=2.0.0",
    "nodes": {
      "fft-analysis": "nodes/fft-analysis.js"
    }
  }
}

Clear API Documentation
#

For every node, I document:

  • Inputs: what msg properties are expected
  • Outputs: what msg properties are set
  • Configuration: what each config field means
  • Errors: what can go wrong and how to fix it

This goes in the node’s HTML help panel (the sidebar in the Node-RED editor) and in the README.


Publishing
#

npm Publish Workflow
#

My publishing checklist:

  1. Update version in package.json (follow semver)
  2. Update CHANGELOG.md
  3. Run full test suite: npm test
  4. Build if needed (most Node-RED nodes don’t need a build step)
  5. Dry run: npm publish --dry-run
  6. Publish: npm publish
  7. Create GitHub release with tag
  8. Post update to Node-RED forum

Semantic Versioning
#

Version BumpWhenExample
Patch (0.0.x)Bug fixes, no API changesFixed FFT window function edge case
Minor (0.x.0)New features, backward compatibleAdded envelope analysis node
Major (x.0.0)Breaking changesChanged message output format

I learned this the hard way — see “Mistakes” section below.

Automated Publishing with GitHub Actions
#

# .github/workflows/publish.yml
name: Publish to npm

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'

      - run: npm ci
      - run: npm test

      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Workflow: create a GitHub Release → tag triggers the action → tests run → npm publish.


Community
#

Handling Issues
#

Most issues fall into three categories:

Issue type           Frequency   Response
──────────────────────────────────────────────────────
Installation help    40%         Point to docs, ask for
                                 Node-RED and Node.js version

"How do I..."        35%         Answer if quick, point to
                                 examples if complex

Actual bugs          20%         Reproduce, fix, release patch

Feature requests     5%          Discuss, add to backlog or
                                 explain why not

Key lesson: Respond quickly, even if the response is “I’ll look into this next week.” Silence kills open-source projects.

Pull Requests from Strangers
#

I’ve received about 15 PRs across all packages. Most were small fixes (typos, README improvements). A few were significant features. For every PR:

  1. Thank the contributor (always, even if you can’t merge)
  2. Review within a week
  3. If changes needed, be specific — “could you add a test for this case?” not “needs work”
  4. Merge and release quickly after approval

The Node-RED Forum
#

The Node-RED Discourse forum is the primary community hub. Posting about your package in the “Share your nodes” category is the single best way to get initial users. The community is welcoming, constructive, and full of people building real things.


Mistakes I Made
#

Over-Engineering v1
#

My first version of condition-monitoring had a plugin architecture, configurable processing pipelines, and abstract signal processor classes. It was flexible enough to analyze anything. Nobody understood how to use it.

v0.1 (over-engineered):

  User → Configure Pipeline:
    1. Select windowing function
    2. Choose FFT algorithm
    3. Set overlap percentage
    4. Define frequency bands
    5. Configure peak detection params
    6. Set alarm thresholds per band
    7. Enable/disable envelope analysis

  User reaction: "I just want to know if my motor is vibrating too much"

v0.2 was a complete rewrite with sensible defaults:

v0.2 (simplified):

  User → Drop FFT node → Set sample rate → Connect to chart
  (3 clicks, works immediately with defaults)

  Advanced? Open settings → tweak parameters

Lesson: Ship the simplest thing that works. Add complexity when users ask for it, not when you imagine they might need it.

Breaking Changes Without Major Version Bumps
#

In nats-suite 0.0.2, I changed the command interface from msg.payload.command to msg.command. This broke existing flows for everyone who upgraded.

I should have:

  1. Signalled it as a breaking change, not folded it into a patch release
  2. Added a migration guide
  3. Kept backward compatibility with a config option

Instead, I got five frustrated issues in one week. Don’t do this.

Ignoring Edge Cases
#

The OPC-UA suite worked perfectly with Siemens PLCs — which is what I tested against. Then someone tried it with a Beckhoff TwinCAT system that returns null for disconnected sensors instead of a status code. Crash.

Another user had a PLC that returned arrays of 50,000 values in a single read. Out of memory.

What I testedWhat users did
10 variables500 variables
English node namesChinese node names (UTF-8 handling)
Single server3 servers simultaneously
Clean datanull, NaN, Infinity, empty strings
Fast networkSatellite link with 800 ms latency
LinuxWindows (path separator issues)

Lesson: Every assumption you make about the environment will be violated by at least one user.

Not Writing Tests First
#

For the first two packages, I wrote tests after the code. This meant:

  • Tests only covered the happy path (because I unconsciously tested what I knew worked)
  • Refactoring was scary (no safety net during the rewrite)
  • Edge cases were discovered in production, not in CI

For condition-monitoring, I wrote tests first. The difference was dramatic — I caught 12 bugs during development that would have shipped otherwise.


What Drove Growth
#

Download Numbers Over Time
#

node-red-contrib-condition-monitoring — monthly downloads

Monthly downloads over 24 months1369121824Month240220200180160140120100806040200Downloads / month
Monthly downloads over 24 months1369121824Month240220200180160140120100806040200Downloads / month

Key moments:

MonthEventDownloads/month
1Published, posted on Node-RED forum30
3Someone blogged about it80
6Added example flows, improved README120
70.3.0 — the LLM analyzer node160
18Referenced in a conference talk210

What Drove Downloads
#

FactorImpactNotes
Node-RED forum postHighFirst wave of users always came from here
Good README with screenshotHighPeople install what they can understand
Example flowsHighLowers the “how do I start” barrier
Consistent publishingMediumRegular updates signal the project is alive
Responding to issuesMediumBuilds trust, users recommend to others
Blog posts about the packageMediumSEO brings in organic search traffic
Conference mentionsHighestSingle biggest spike in downloads

What Did NOT Drive Downloads
#

  • Twitter/X posts (IIoT people aren’t on tech Twitter)
  • Complex feature additions (users want simplicity)
  • README badges (nobody cares about your coverage badge)

What’s Next
#

PackageNext Steps
condition-monitoringBearing defect frequency calculator, additional ISO standard presets
nats-suiteKV store support, object store integration
opcua-suiteAlarms & conditions, method call improvements
clab-interfacesAlignment with CompuLab v2 API changes
i3xHistorical data aggregation, batch operations

The overarching theme: do fewer things, better. Every package has a feature request backlog longer than what I’ll ever build. The discipline is saying no to complexity and yes to reliability.


Advice for First-Time Package Authors
#

  1. Start with your own itch. Build what you need, then generalize.
  2. Ship early. A working v0.1 with three nodes beats a perfect v1.0 that never ships.
  3. Write tests from day one. Future you will thank present you.
  4. README is marketing. Spend as much time on the README as on the code.
  5. Semantic versioning is a promise. Breaking that promise breaks trust.
  6. Respond to issues. A one-sentence acknowledgment within 48 hours goes a long way.
  7. Don’t chase downloads. Solve a real problem well, and users will find you.
  8. Accept that most code will be rewritten. v1 exists to teach you what v2 should be.

Half a year, seven packages, hundreds of tests, and thousands of downloads later — the most valuable thing I’ve built isn’t the code. It’s the practice of building, shipping, and maintaining software that other people depend on. That practice transfers to every project, every team, every role. Start your own package. Today.

IIoT - This article is part of a series.
Part 14: This Article

Related