DEV Community

Cover image for Stop Hardcoding Business Logic: Meet Rule Engine JS
Crafts 69 Guy
Crafts 69 Guy

Posted on

Stop Hardcoding Business Logic: Meet Rule Engine JS

Table of Contents


Have you ever found yourself buried in nested if-else statements, trying to implement complex business rules that change every other week? Or worse, watched your clean codebase turn into a maintenance nightmare because business logic was scattered across dozens of files?

I've been there. And that's exactly why I built Rule Engine JS.

The Problem That Started It All

Picture this: You're building an e-commerce platform, and the business team comes to you with "simple" requirements:

  • VIP customers get discounts on orders over $100
  • Regular customers need 1000+ loyalty points for discounts
  • First-time customers get 20% off orders over $50
  • But only during business hours
  • Unless it's a weekend
  • Or a holiday
  • And the rules change next month...

Sound familiar? 😅

The real kicker came when they said: "Oh, and we need to store these rules in the database so we can update them without deploying."

That's when I realized I needed something that could handle JSON-serializable business logic without sacrificing developer experience or performance.

Enter Rule Engine JS

After trying existing solutions and finding them either too complex, too slow, or lacking key features, I decided to build something better. Here's what makes Rule Engine JS different:

🚀 Zero Dependencies, Maximum Performance

No bloated dependency trees. No security vulnerabilities from third-party packages. Just clean, efficient JavaScript that works everywhere:

import { createRuleEngine, createRuleHelpers } from 'rule-engine-js';

const engine = createRuleEngine();
const rules = createRuleHelpers();

// That's it. You're ready to go.
Enter fullscreen mode Exit fullscreen mode

🎯 Developer Experience That Actually Makes Sense

Remember those nested JSON rules that make your eyes bleed? Not here:

// Instead of this nightmare:
const rule = {
  "and": [
    { "gte": ["user.age", 18] },
    { "eq": ["user.status", "active"] },
    { "in": ["write", "user.permissions"] }
  ]
}

// Write this:
const rule = rules.and(
  rules.gte('user.age', 18),
  rules.eq('user.status', 'active'),
  rules.in('write', 'user.permissions')
);
Enter fullscreen mode Exit fullscreen mode

Both compile to the same JSON (perfect for database storage), but only one is actually maintainable.

Dynamic Field Comparison (The Game Changer)

Here's where things get interesting. Need to compare fields within your data? Most rule engines make you jump through hoops. Rule Engine JS makes it natural:

const orderValidation = rules.and(
  rules.field.lessThan('order.total', 'user.creditLimit'),
  rules.field.equals('order.currency', 'user.preferredCurrency'),
  rules.gte('user.accountAge', 30) // days
);
Enter fullscreen mode Exit fullscreen mode

This was the feature I desperately needed for my original project - and now it's built right in.

Real World Examples

Let's solve that e-commerce discount problem from earlier:

const discountEligibility = rules.or(
  // VIP customers with minimum order
  rules.and(
    rules.eq('customer.type', 'vip'),
    rules.gte('order.total', 100)
  ),

  // High loyalty points
  rules.gte('customer.loyaltyPoints', 1000),

  // First-time customer bonus
  rules.and(
    rules.isTrue('customer.isFirstTime'),
    rules.gte('order.total', 50)
  )
);

// Later, when business rules change:
const updatedRules = await database.getRules('discount-eligibility');
const result = engine.evaluateExpr(updatedRules, customerData);
Enter fullscreen mode Exit fullscreen mode

No deployments. No code changes. Just update the database and you're done.

Form Validation Made Simple

const formValidation = rules.and(
  rules.validation.required('email'),
  rules.validation.email('email'),
  rules.field.equals('password', 'confirmPassword'),
  rules.validation.ageRange('age', 18, 120),
  rules.isTrue('agreedToTerms')
);

// Returns detailed validation results
const result = engine.evaluateExpr(formValidation, formData);
if (!result.success) {
  console.log('Validation failed:', result.error);
}
Enter fullscreen mode Exit fullscreen mode

User Access Control

const accessRule = rules.and(
  rules.isTrue('user.isActive'),
  rules.or(
    rules.eq('user.role', 'admin'),
    rules.and(
      rules.eq('user.department', 'resource.department'),
      rules.in('read', 'user.permissions')
    )
  )
);

// Perfect for middleware
app.get('/api/sensitive-data', (req, res, next) => {
  const hasAccess = engine.evaluateExpr(accessRule, {
    user: req.user,
    resource: { department: 'finance' }
  });

  hasAccess.success ? next() : res.status(403).json({ error: 'Access denied' });
});
Enter fullscreen mode Exit fullscreen mode

Built for Production

Performance That Scales

  • Intelligent LRU caching for repeated evaluations
  • Path resolution caching for nested object access
  • Regex pattern caching for validation rules
  • Typically under 1ms evaluation time

Security First

  • Built-in protection against prototype pollution
  • Safe path resolution (no function execution)
  • Input validation and sanitization
  • Configurable complexity limits to prevent DoS

Monitoring Included

// Built-in performance metrics
const metrics = engine.getMetrics();
console.log({
  evaluations: metrics.evaluations,
  cacheHitRate: metrics.cacheHits / metrics.evaluations,
  averageTime: metrics.avgTime + 'ms'
});
Enter fullscreen mode Exit fullscreen mode

Framework Agnostic

Whether you're using React, Vue, Express, Next.js, or vanilla JavaScript - Rule Engine JS just works:

// React
function useRuleValidation(rules, data) {
  return useMemo(() => 
    engine.evaluateExpr(rules, data), [rules, data]
  );
}

// Express middleware
const createAccessMiddleware = (rule) => (req, res, next) => {
  const result = engine.evaluateExpr(rule, req.user);
  result.success ? next() : res.status(403).end();
};

// Vue computed property
computed: {
  isEligible() {
    return engine.evaluateExpr(this.businessRules, this.userData).success;
  }
}
Enter fullscreen mode Exit fullscreen mode

Getting Started (30 Seconds)

npm install rule-engine-js
Enter fullscreen mode Exit fullscreen mode
import { createRuleEngine, createRuleHelpers } from 'rule-engine-js';

const engine = createRuleEngine();
const rules = createRuleHelpers();

// Your first rule
const canAccess = rules.and(
  rules.gte('user.age', 18),
  rules.eq('user.status', 'active')
);

// Test it
const result = engine.evaluateExpr(canAccess, {
  user: { age: 25, status: 'active' }
});

console.log(result.success); // true
Enter fullscreen mode Exit fullscreen mode

That's it. No configuration files, no complex setup - just clean, working code.

Why I Think You'll Love It

After using Rule Engine JS in production for several months, here's what I appreciate most:

  1. It stays out of your way - Simple API, predictable behavior
  2. Debugging is actually pleasant - Clear error messages and built-in logging
  3. Performance is transparent - See exactly how your rules are performing
  4. Security is handled - Protection against common attacks built-in
  5. It scales with your needs - From simple validation to complex business logic

What's Next?

I'm actively working on Rule Engine JS and would love your feedback. Whether you're dealing with complex business rules, form validation, or access control - give it a try and let me know what you think.

Star the repo: github.com/crafts69guy/rule-engine-js

📦 Try it now: npm install rule-engine-js

📚 Full docs: Comprehensive guides and examples included

🐛 Issues: Found a bug or have a feature request? I'd love to hear from you!


Rule Engine JS is MIT licensed and has zero dependencies. It works in Node.js 16+ and all modern browsers. Built by developers, for developers who are tired of hardcoding business logic.

Top comments (5)

Collapse
 
__f5a498b7 profile image
Никита Балобанов

It looks great! At my work, I do something similar, but in your code I found many interesting ideas, that I hadn't thought about. And now I can’t wait for Monday to try to implement them 😅

I have a question for you as a specialist who wrote the implementation of this rule system much better than me ☺️. If you don't mind, of course.

On top of rules executor, I have created event system that runs predefined action when a rule is “triggered”. By “triggered”, I mean when the rule's result switches from false to true.

In a system like that, users want access to a few specific operators: “changed”, “changed by”, and “changed from”. There are two problems with these.

The first one is that we need to pass the previous state of the context to the evaluate function. Without that, we would need to parse and modify each rule manually before evaluating them.

The second one is that we need to know if the “changed” operator returns “true”, because, unlike other operators, it must trigger on every run where the value is different from before.

What do you think about these cases?

Collapse
 
crafts69guy profile image
Crafts 69 Guy • Edited

So, I'm late, but I'm excited to tell you that the architecture I've built actually addresses both of those challenges head-on! 😊

Looking at your requirements, I can see you're dealing with the exact same problems I faced when building this system. Here's how I solved them:

Problem 1: Passing Previous State ✅ Solved
I created a StatefulRuleEngine wrapper that automatically manages state transitions. Instead of manually modifying rules, it enriches the context:

// The StatefulRuleEngine automatically adds previous state
const enrichedContext = {
  ...context,
  _previous: previousContext,  // 👈 Previous state automatically injected
  _meta: { hasChangeOperator: false }
};
Enter fullscreen mode Exit fullscreen mode

Problem 2: Change Operators & Triggering Logic ✅ Solved
I implemented all the change operators you mentioned in src/operators/state.js

// Your exact operators!
{ changed: ["user.status"] }           // Detects any change
{ changedBy: ["temperature", 5] }      // Numeric change by amount  
{ changedFrom: ["status", "pending"] } // Changed from specific value
{ changedTo: ["status", "completed"] } // Changed to specific value
Enter fullscreen mode Exit fullscreen mode

The brilliant part is the automatic triggering logic. The system detects when change operators are used and adjusts the triggering behavior:

// In StatefulRuleEngine.js
shouldTrigger(previousResult, currentResult, hasChangeOperator, isPureChangeRule, options = {}) {
  if (hasChangeOperator) {
    if (triggerOnEveryChange) {
      return currentResult.success;  // 👈 Triggers on every change!
    } else {
      return !previousResult.success && currentResult.success;  // Default behavior
    }
  }
  // Standard false→true logic for regular operators
  return !previousResult.success && currentResult.success;
}
Enter fullscreen mode Exit fullscreen mode

Real Example

import { createRuleEngine, StatefulRuleEngine } from 'rule-engine-js';

const engine = createRuleEngine();
const statefulEngine = new StatefulRuleEngine(engine, {
  triggerOnEveryChange: true  // 👈 Perfect for your "changed" operators
});

// Your rule with change detection
const rule = {
  or: [
    { changed: ["user.status"] },           // Triggers every time status changes
    { changedFrom: ["user.role", "guest"] } // Triggers when role changes from "guest"
  ]
};

// Event handling - exactly what you need!
statefulEngine.on('triggered', (eventData) => {
  console.log('Rule triggered!', eventData.ruleId);
  // Run your predefined actions here
});

// Usage - no manual state management needed
statefulEngine.evaluate('user-status-rule', rule, currentContext);
Enter fullscreen mode Exit fullscreen mode

Please check it out beta version here
Is this a match with your case?

Collapse
 
__f5a498b7 profile image
Никита Балобанов

And again, your solution looks better 😁 Maybe instead of rewriting my code, I’ll try using your library 👍

Thread Thread
 
crafts69guy profile image
Crafts 69 Guy

Thanks a ton 🤠
I just whipped up this idea with huge help from Claude Code. That thing's crazy powerful - you gotta try it! Btw, the implementation above is just a beta 🤓. Let's test it out and hit me with your feedback!

Collapse
 
crafts69guy profile image
Crafts 69 Guy

Thanks so much for the kind words! 😊 I'm glad you found some useful ideas in your implementation!

Your question about state change operators is great - this is actually an interesting architectural challenge that I've been thinking about as well. You've pinpointed two pain points:

Accessing previous state - Operators need to compare the current value to the previous value, meaning we need to pass the previous context through the evaluation pipeline somehow.

Different trigger semantics - Unlike regular operators that trigger on the transition from false to true, "changed" operators need to be triggered on every change.

I think the current architecture of the Rule Engine can handle this well, and I can give you a solution tonight, so stay tuned 😎