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 (microseconds, but you don’t want to 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';

export const engine = new Engine();   // one per process
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 JSON or invalid structure
    cache.set(key, rule);
  }
  return rule;
}

Compiled Rule objects are immutable and safe to share across requests, 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. (They cannot be posted to worker_threads; a worker loads the module and keeps its own cache.)

The endpoint

// app.js
import express from 'express';
import { engine, 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 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 {
    // JSON, structure, operator names and argument counts
    compiled = engine.compileChecked(req.body.logic);
  } catch (err) {
    return res.status(422).json({ error: 'invalid rule', diagnostics: err.diagnostics });
  }
  // run the rule against golden cases before activating it
  try {
    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' });
      }
    }
  } catch (err) {
    return res.status(422).json({ error: `rule fails its test cases: ${err.message}` });
  }
  // ...persist row with a bumped version...
  res.sendStatus(204);
});

compile rejects malformed JSON and structurally invalid rules (for example a multi-key object when templating is off), but it does not verify operator names: { "discuont": [...] } compiles fine and throws errorType: 'InvalidOperator' only when you evaluate the rule. compileChecked runs the engine’s check first and refuses the rule with a CompileError whose diagnostics name each problem and its JSON Pointer (for the typo above, code UnknownOperator at pointer ""), so an admin sees the typo when saving the rule.

compiled.facts().reads lists the data paths the rule reads ([['cart', 'total'], ['user', 'tier']]), so ingestion can also refuse a rule that reads fields the endpoint never supplies. To make a typo in a path fail instead of reading null, build the engine with config: { missing_var: 'error' }; such a read then throws errorType: 'VariableNotFound'.

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 check-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

A typical rule evaluates in under a 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, you can preview the exact rule your Express service enforces in the browser with @goplasmatic/datalogic-wasm, or render and step-debug it with the React visual editor. Nothing gets re-implemented, so what the UI shows matches what the API decides.

Error handling

compile and evaluate throw real Error objects with a stable errorType tag ("ParseError", "InvalidOperator", "Thrown", "InvalidArguments", …) and operator, the innermost operator that failed. compileChecked adds "CompileError", and the typed session methods (evaluateBool / evaluateInt / evaluateFloat) add "TypeMismatch" for a result of the wrong type. A panic in the native engine arrives as "InternalError" instead of taking the process down; treat it as a server error. Map them in your Express error middleware:

app.use((err, req, res, next) => {
  if (err.errorType === 'InternalError') return next(err); // an engine bug: 500
  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, batch and async evaluation, tracing, operator names, rule facts).

See Also