Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Integration: Express (Node.js)

This guide wires @goplasmatic/datalogic-node into an Express service the way it wants to be used in production: one engine for the process, rules compiled once and cached, evaluation per request, with the async tier keeping heavy evaluations off the event loop.

The running example is a discount service: rules live in a database column as JSONLogic, product/ops people change them without a deploy, and the API applies whichever rule is active.

Install

npm install express @goplasmatic/datalogic-node

The package ships native prebuilds for eight platforms: no Rust toolchain or node-gyp involved.

A rule service: compile once, cache by version

Compiling is the expensive step (still microseconds, but don’t pay it per request). Cache compiled rules keyed by their identity, and let a rule update replace the cache entry:

// rules.js
import { Engine } from '@goplasmatic/datalogic-node';

const engine = new Engine();          // one per process; thread-safe
const cache = new Map();              // ruleId@version -> compiled Rule

export function getRule(row) {
  // row: { id, version, logic } from your DB
  const key = `${row.id}@${row.version}`;
  let rule = cache.get(key);
  if (!rule) {
    rule = engine.compile(row.logic); // throws on malformed rules
    cache.set(key, rule);
  }
  return rule;
}

Compiled Rule objects are immutable and safe to share, so a plain Map is all the machinery you need. If rules churn, swap the Map for an LRU: compiled rules are cheap to rebuild.

The endpoint

// app.js
import express from 'express';
import { getRule } from './rules.js';
import { loadActiveDiscountRule } from './db.js';

const app = express();
app.use(express.json());

app.post('/quote', async (req, res, next) => {
  try {
    const row = await loadActiveDiscountRule();
    const rule = getRule(row);
    const total = rule.evaluate({ cart: req.body.cart, user: req.user });
    res.json({ total });
  } catch (err) {
    next(err);
  }
});

rule.evaluate(data) takes and returns plain JS objects: no manual JSON stringify/parse on your side.

Validating rules at ingestion, not at request time

The moment users or admins can author rules, treat rule ingestion like any other untrusted input path:

app.put('/rules/:id', express.json({ limit: '64kb' }), (req, res) => {
  let compiled;
  try {
    compiled = engine.compile(req.body.logic); // syntax + operator check
  } catch (err) {
    return res.status(422).json({ error: `invalid rule: ${err.message}` });
  }
  // run the rule against golden cases before activating it
  for (const [input, expected] of req.body.tests ?? []) {
    if (JSON.stringify(compiled.evaluate(input)) !== JSON.stringify(expected)) {
      return res.status(422).json({ error: 'rule fails its test cases' });
    }
  }
  // ...persist row with a bumped version...
  res.sendStatus(204);
});

Two things are doing security work here: the size limit on the body (a hostile 10 MB rule is safe to compile but not free), and the compile-then-test gate. Evaluation itself is sandboxed: rules have no I/O, no eval, and can only read the data document you pass.

Keeping big evaluations off the event loop

Evaluations are typically sub-microsecond, so the sync call is right for most endpoints. For large payloads or batch endpoints, use the async tier. It runs on the libuv thread pool:

app.post('/evaluate-batch', async (req, res, next) => {
  try {
    const rule = getRule(await loadActiveDiscountRule());
    const results = await Promise.all(
      req.body.items.map((item) => rule.evaluateStrAsync(JSON.stringify(item)))
    );
    res.json(results.map((r) => JSON.parse(r)));
  } catch (err) {
    next(err);
  }
});

Letting the frontend preview the same rule

Because every binding runs the same core, the exact rule your Express service enforces can be previewed in the browser with @goplasmatic/datalogic-wasm, or rendered and step-debugged with the React visual editor: no re-implementation, no drift between what the UI shows and what the API decides.

Error handling

compile and evaluate throw real Error objects with a stable errorType tag ("ParseError", "TypeMismatch", "Thrown", …). Map them in your Express error middleware:

app.use((err, req, res, next) => {
  if (err.errorType === 'ParseError') return res.status(422).json({ error: err.message });
  if (err.errorType) return res.status(400).json({ error: err.message });
  next(err);
});

See the Node.js chapter for the full API surface (sessions, data handles, typed results, tracing).