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
- Node.js Native (
@goplasmatic/datalogic-node) — full Node.js API reference - Starter Boilerplates — runnable project templates for every language
- Sandboxing Untrusted Rules — security checklist for user-authored rules
- Rule Analysis —
check,compileChecked, andfactsin detail - Integration: Spring Boot (JVM) — the same architecture in a Java/Spring service