Managing Context in Express Application

Uncategorized
2.9k words

Pre-requisite

  • NodeJs

Why?

When creating a Web API, there is a need to manage a client’s context so that when it’s required in a function, the function gets the correct context and processes the data.

To achieve that, we often pass parameters to each function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//illustration

function (req, res) {
const user = req.user
validateuser(user);
}

function validateUser(user) {}

function getProducts(user) {
updateUserLastVisit(user);
}

function updateUserLastVisit(user) {}

Of course, it’s a very simple example.

Notice that getProducts(user) doesn’t need a user but since updateUserLastVisit(user) requires user data the function needed it.

What if we can store the user context and just get it when we need it down the function call?

enter continuation local storage. In simple terms, it allows us to store data for each request and delete it automatically when the chain call is finished.

Let’s start by creating a blank NodeJs project

1
2
3
yarn init -y
# or
npm init -y
1
2
# add express and continuation local storage dependencies
yarn add express continuation-local-storage

Create src/index.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import Express from "express";
import Cls from "continuation-local-storage";

const PORT = 3000;

const app = Express();

const createNameSpace = Cls.createNamespace;
const getNameSpace = Cls.getNamespace;

const userCreateNameSpace = createNameSpace("user-session"); //namspace to create session so we can store it
const userNameSpace = getNameSpace("user-session"); //namespace to receive session data

app.use("/", function(req, res, next) {
userCreateNameSpace.run(() => { //need to run the name space
const randomNumber = Math.random();
userCreateNameSpace.set("user", { name: `John Doe #${randomNumber}`}); //register current user to session
next();
});
});

app.get("/", (req, res) => {
const products = getProducts(); //get product without passing user
return res.send("Hello World");
});

function validateUser(user) {
console.log(`validating user ${user.name}`);
}

function getProducts() {
updateUserLastVisit(); //getProducts don't need to know about user
return ["Product Foo", "Product Bar"]
}

function updateUserLastVisit() {
const user = userNameSpace.get("user"); //get current user data anywhere
validateUser(user);
console.log(`Updating ${user.name} last visit data`);
}


app.listen(PORT, () =>{
console.log(`Hey the server is running on port ${PORT}`)
})

Try running it with

1
2
3
4
node src/index.js

#then call the API
curl http://localhost:3000

There are going to be tradeoffs, one of which is testing, now you need to mock/stub the namespace call if you are running a unit test.

Conclusion

When you are building a complex application, you need to make a trade-off on how to manage your code, if you pass every variable that’s needed down in the function call even though your function might not need it your code will be harder to read, on the other hand, if you use context-based session like the one we did above, you will need extra effort on the test.

View code on github