Hi folks!
I’ve been working on a personal project, a simple Express.js REST API. But after a while, I got stuck on a simple problem: What is the best way to manage user permissions to certain routes in certain circumstances?
You may think that I could just add a bunch of if
else
in many different places to achieve that, but it would be a pain to debug, test, and mainly, change (trust me!).
So, I’ve come up with a solution that makes use of the functional paradigm, which is very powerful. In general, it was a nice solution that can be useful in real situations and I wanted to share this with you guys in hopes that you’ll make more use of this wonderful paradigm. Enjoy it!
Funcional programming
First of all, let’s see what is functional programming and some core concepts that will be used in this example.
Functional programming is a way of writing code that uses pure functions, instead of classes, avoiding state changes and side effects.
It uses mathematical thinking of functions: they take an input and return an output. This approach has several benefits, like making it easier to write tests, as functions’ behavior depends only on their parameters, and it forces modularity.
But, in functional programming, these function inputs and outputs can be other functions, not just values, and that’s exactly where this strategy becomes powerful.
Composition
Composition is the process of adding the result of a certain function as the input of another. This way, we can build little pieces of software that are reusable and that have only one responsibility (as it should always be).
const upperCase = (text) => text.toUpperCase();
const reverse = (text) => text.split("").reverse().join("");
// Composed function
const reverseAndUpperCase = (text) => upperCase(reverse(text));
reverseAndUpperCase("hello"); // OLLEH
Higher order functions
These are special kinds of functions that work with other functions: they can take them as arguments or return a new function.
const applyLogger =
(func) =>
(...values) => {
console.log("called");
return func(...values);
};
const sayHello = applyLogger((name) => console.log(Hello ${name}
));
sayHello("John");
// called
// Hello John
After seeing these, you might think "Ok, but how will it help me in my daily life?" and I think that is the part that most teachers get stuck on: they just don't apply the concepts and make it all sound useless. So, let's get our hands on some code!
CODE!
Setup
Let's do a quick ExpressJs project setup:
yarn init -y && yarn install express moment
// src/server.js
const express = require("express");
const app = express();
app.get("/actions/user", (req, res) => res.json({ message: "User action" }));
app.get("/actions/admin", (req, res) => res.json({ message: "Admin action" }));
app.listen(3000);
Now, if we run node src/server.js
, our server will run at localhost:3000.
The problem
Note that in this application there’re only 2 endpoints (/action/admin
and /action/user
). In the real world though, there could be a lot more, and some of them may have some really specific, but similar, permission rules, especially when you’re dealing with monoliths.
Just imagine that you’re working in a feature, like a new route, which has some pretty similar permission rules to another existing route, but applied in a different order, or that needs to do one extra step of validation and, at the end of the day, you need to write an entire functionality that does the almost same thing that has been written before. Yeah, that’s not the best way to go.
The general approach of putting all validation in a huge function hurts the open-close principle and the single responsibility principle. Also changing working code, possibly not written by you, can lead to bugs and unexpected behavior.
Permission rules
/actions/user
: someone can access this if they’re a User, an Admin, or, send the correct time in theHH:mm
format./actions/admin
: someone can access this if they’re an Admin or a User and has sent the right time in theHH:mm
format.
As this is a simple example, I won’t be implementing a User system. This identification will be done just by sending an x-identifier-token
with a UUID token. The time will be received in the x-time
header.
Making the permission system
For that, we need to make a permission maker: this is a high order function that creates a middleware that runs other permissions and lets it pass if any of them returns true, otherwise it returns a 401 status.
OBS: It is important to keep this and|or
logic in mind to make it right.
Maybe some of you have already noticed, but this behavior sounds pretty much like a composition problem, and that’s exactly what it is. So, let’s make a composer that applies the or
logic:
// src/lib/index.js
const orCompose =
(...funcs) =>
(value) =>
funcs.reduce((acc, func) => acc || func(value), false);
module.exports = { orCompose };
This orCompose
function takes other functions, that should return a boolean value, as inputs and returns a new function that runs each function sequentially taking their results, passing them to the next iteration, and making comparisons. Therefore, we can combine behavior in a or
logic.
Note that if one of the functions returns true
, the following ones won’t run.
// Example
const is1 = (value) => value === 1;
const is2 = (value) => value === 2;
const composed = orCompose(is1, is2);
composed(1); // true, is1 return true
composed(2); // true, is2 return true
composed(3); // false, both return false
Back to the main problem… In a real hole system, probably async operations (such as Database queries) would be done, so I’ll change this a little so it can handle promises too.
// src/lib/index.js
const orCompose =
(...funcs) =>
(value) =>
funcs.reduce(
(acc, func) =>
Promise.resolve(acc).then((result) => result || func(value)),
false
);
module.exports = { orCompose };
Now, let’s create the individual rules. We need to check, through the request object, to determine if the client is a User, an admin, has sent the right time, or is a user and has sent the right time:
// src/permission.js
const moment = require("moment");
// These should be env variables
const secretKeys = {
ADMIN: "e055214a-5116-4da6-8419-0e85c88609a6",
USER: "cf798a4f-2d8c-48ed-b56f-ae19c786c3be",
};
const isUser = async (req) =>
req.headers["x-identifier-token"] === secretKeys.USER;
const isAdmin = async (req) =>
req.headers["x-identifier-token"] === secretKeys.ADMIN;
const isRightTime = async (req) =>
req.headers["x-time"] === moment().format("HH:mm");
const isUserAndIsRightTime = async (req) =>
(await isAdmin(req)) && (await isUser(req));
Note that each function has its pretty well-defined responsibility, which allows us the reuse them wherever we want, as well as test them individually.
To finish this, let’s create a makePermission
function:
// src/permission.js
const { orCompose } = require("./lib");
// ...
const makePermission =
(...permissions) =>
async (req, res, next) => {
const checker = orCompose(...permissions);
const result = await checker(req, res, next);
if (result) return next();
return res.status(401).send();
};
// ...
So, now we can just use makePermission
to build a middleware that applies the given rules and use them in our server.js
file.
// src/permission.js
// ...
module.exports = {
userPermission: makePermission(isUser, isAdmin, isRightTime),
adminPermission: makePermission(isAdmin, isUserAndIsRightTime),
};
// src/server.js
const express = require("express");
const { userPermission, adminPermission } = require("./permission");
const app = express();
app.get("/actions/user", userPermission, (_req, res) =>
res.json({ message: "User action" })
);
app.get("/actions/admin", adminPermission, (_req, res) =>
res.json({ message: "Admin action" })
);
app.listen(3000, () => console.log("Server running at 3000!"));
And now we have a working permission system with logic that isn’t based on a bunch of if/else
statements!
BONUS
As I said, this and|or
logic is important in this case. Our system is great for or
logics, but not so much when it comes to and
logics. This can be seen in the isUserAndRightTime
function:
// src/permission.js
const isUserAndIsRightTime = async (req) =>
(await isAdmin(req)) && (await isUser(req));
To avoid this ugly piece of code, we can create a Composer with an and
logic, just like we’ve done before:
// src/lib/index.js
const andCompose =
(...funcs) =>
(value) =>
funcs.reduce(
(acc, func) =>
Promise.resolve(acc).then((result) => result && func(value)),
true
);
const orCompose =
(...funcs) =>
(value) =>
funcs.reduce(
(acc, func) =>
Promise.resolve(acc).then((result) => result || func(value)),
false
);
module.exports = { andCompose, orCompose };
Using these, we can use any combination of permissions to build complex statements (just like the logic operators &&
and ||
).
Now, let’s refactor our previous code.
// src/permission.js
const moment = require("moment");
const { andCompose, orCompose } = require("./lib");
const secretKeys = {
ADMIN: "e055214a-5116-4da6-8419-0e85c88609a6",
USER: "cf798a4f-2d8c-48ed-b56f-ae19c786c3be",
};
const makePermission =
(...permissions) =>
async (req, res, next) => {
const checker = orCompose(...permissions);
const result = await checker(req, res, next);
if (result) return next();
return res.status(401).send();
};
const isUser = async (req) =>
req.headers["x-identifier-token"] === secretKeys.USER;
const isAdmin = async (req) =>
req.headers["x-identifier-token"] === secretKeys.ADMIN;
const isRightTime = async (req) =>
req.headers["x-time"] === moment().format("HH:mm");
module.exports = {
userPermission: makePermission(isUser, isAdmin, isRightTime),
adminPermission: makePermission(isAdmin, andCompose(isUser, isRightTime)),
};
And that’s pretty much it!
Final Thoughts
On this link, you can access the code produced during this post to see the full version.
I hope you’ve enjoyed this dive and that you now can see the power of using functional programming concepts in real-world problems.
Useful links
- High Order Functions
- Functional JavaScript: Function Composition For Every Day Use
- Introduction to Functional Programming with Javascript
We want to work with you. Check out our "What We Do" section!