Separate auth logic with pure predicates and reusable higher‑order wrappers, and compose rules using OR/AND operators.
In modern JavaScript/TypeScript applications, logic such as authentication, validation, or feature‑gating often lives outside the core business functions.
A clean way to keep this cross‑cutting concern separate is to use higher‑order functions (HOFs)—functions that receive a function and return a new, wrapped version of it.
Below we walk through a real‑world scenario:
- We have a service method
ListKeys({ userId, skipChecks, organizationId }). - The method should throw an error unless either the user is a specific admin (
userId === 'a') or the organization is a specific admin (organizationId === 'b'), unless the caller explicitly opts out withskipChecks.
We’ll see three steps:
- A simple “decorator” HOF for a single check
- Extracting pure check functions so they can be freely combined.
- Composing two checks with an OR condition (and a generic combinator for future extensions).
1️⃣ A Basic Higher‑Order Function (HOF) Decorator
A decorator (in the functional sense) receives the original function and returns a wrapped version that performs the extra work before delegating to the original logic.
// Basic HOF that adds an auth check
function withAuthCheck(fn) {
return function (args) {
const { userId, skipChecks, organizationId } = args;
if (!skipChecks) {
// Single condition: user must be 'a' AND org must be 'b'
if (userId !== 'a' && organizationId !== 'b') {
throw new Error('Unauthorized: Invalid user or organization');
}
}
// Call the original function with the (unchanged) arguments
return fn(args);
};
}
// Original business logic
function ListKeys({ userId, skipChecks, organizationId }) {
// …real work here…
return `Keys for user ${userId} in org ${organizationId}`;
}
// Decorated version ready for use
const SecureListKeys = withAuthCheck(ListKeys);
Pros
- Extremely straightforward – a single wrapper around the logic.
- Works everywhere, no special compiler flags required.
Cons
- The condition is hard‑coded. If you later need a different combination (e.g., OR instead of AND) you end up writing a new decorator or duplicating code.
2️⃣ Splitting the Check Logic From the Wrapper
To make our checks reusable, we separate the pure predicate functions (which simply return true or false) from the error‑throwing wrapper.
// Pure predicates – no side‑effects, just return a boolean
const isUserAdmin = ({ userId }) => userId === 'a';
const isOrgAdmin = ({ organizationId }) => organizationId === 'b';
Now we can build a generic wrapper that receives any predicate and throws when it fails:
function withPredicateCheck(predicate, errorMessage = 'Unauthorized') {
return function (fn) {
return function (args) {
if (!args.skipChecks && !predicate(args)) {
throw new Error(errorMessage);
}
return fn(args);
};
};
}
Usage with a single check:
const SecureListKeys = withPredicateCheck(isUserAdmin)(ListKeys);
3️⃣ Composing Two Checks With an OR Condition
The heart of the problem is: allow the call if either isUserAdmin or isOrgAdmin is true.
3.1 A Dedicated OR‑decorator
// Accept two predicate functions and return a combined predicate (OR logic)
const or = (predA, predB) => (args) => predA(args) || predB(args);
// Build a wrapper that uses the combined predicate
const withAuthOr = (predA, predB) => withPredicateCheck(
or(predA, predB),
'Unauthorized: Must be user admin OR org admin'
);
Now the final, composable version looks clean:
// Compose the two checks with OR
const SecureListKeys = withAuthOr(isUserAdmin, isOrgAdmin)(ListKeys);
3.2 Full Working Example
// ------------------------------------------------
// 1️⃣ Pure check functions
// ------------------------------------------------
const isUserAdmin = ({ userId }) => userId === 'a';
const isOrgAdmin = ({ organizationId }) => organizationId === 'b';
// ------------------------------------------------
// 2️⃣ Generic combinators
// ------------------------------------------------
const or = (a, b) => (args) => a(args) || b(args);
// ------------------------------------------------
// 3️⃣ Generic wrapper that throws on failure
// ------------------------------------------------
function withPredicateCheck(predicate, errorMsg = 'Unauthorized') {
return (fn) => (args) => {
if (!args.skipChecks && !predicate(args)) {
throw new Error(errorMsg);
}
return fn(args);
};
}
// ------------------------------------------------
// 4️⃣ Specific OR‑based wrapper
// ------------------------------------------------
const withAuthOr = (predA, predB) =>
withPredicateCheck(or(predA, predB), 'Unauthorized: Must be user admin OR org admin');
// ------------------------------------------------
// 5️⃣ Business function
// ------------------------------------------------
function ListKeys({ userId, organizationId }) {
// Real implementation would fetch DB records, etc.
return `Keys for user ${userId} in org ${organizationId}`;
}
// ------------------------------------------------
// 6️⃣ Decorated, ready‑to‑use function
// ------------------------------------------------
const SecureListKeys = withAuthOr(isUserAdmin, isOrgAdmin)(ListKeys);
// ------------------------------------------------
// 7️⃣ Demonstration
// ------------------------------------------------
console.log(SecureListKeys({ userId: 'a', organizationId: 'x' })); // ✅ user admin
console.log(SecureListKeys({ userId: 'x', organizationId: 'b' })); // ✅ org admin
console.log(SecureListKeys({ userId: 'a', organizationId: 'b' })); // ✅ both
try {
SecureListKeys({ userId: 'x', organizationId: 'y' }); // ❌ fails
} catch (e) {
console.error(e.message); // "Unauthorized: Must be user admin OR org admin"
}
// Skip checks altogether – useful for internal scripts or testing
console.log(SecureListKeys({ userId: 'x', organizationId: 'y', skipChecks: true })); // ✅ bypassed
3.3 Extending the Model
Because the predicate logic is now data‑driven, you can build richer combinators:
// AND combinator
const and = (a, b) => (args) => a(args) && b(args);
// NOT combinator
const not = (p) => (args) => !p(args);
// Example: (userAdmin OR orgAdmin) AND NOT isSuspended
const isSuspended = ({ suspended }) => suspended;
const complexRule = and(or(isUserAdmin, isOrgAdmin), not(isSuspended));
Now the same withPredicateCheck wrapper can enforce arbitrarily complex policies without ever touching the core business code.
🧩 Takeaways
| ✅ What we achieved | 🔧 How we did it |
|---|---|
| Separation of concerns – business logic stays pure, auth lives outside. | Pure predicate functions (isUserAdmin, isOrgAdmin). |
| Reusability – same checks can be used in many places. | Generic withPredicateCheck wrapper. |
| Composable security rules – OR, AND, NOT, and any custom combination. | Small combinators (or, and, not). |
Opt‑out flag (skipChecks) – helpful for internal tooling or testing. |
Guard inside the wrapper that bypasses the predicate when skipChecks is true. |
By embracing higher‑order functions as lightweight decorators, you gain a flexible, testable, and declarative way to enforce authorization (or any other cross‑cutting concern) across your codebase. The pattern scales nicely: add new predicates, combine them with logical operators, and wrap any function you need—no decorators from a language‑specific framework required.