Let's imagine that you are building a SaaS platform where companies can manage information about their projects. You need to expose an API where users can see the projects from their company, but your clients don't want to share their projects information with anyone else that is not an employee.
So you start creating a new Express app. First of all, you create a middleware to authenticate the user.
module.exports = (req, res, next) => {
const authorization = req.get('Authorization');
if (!authorization) {
next('No Authorization header');
}
let { userId } = decodeToken(authorization);
let user = UserModel.findById(userId);
req.context = {
user,
};
next();
};
This middleware just verifies the token, extracts the userId
from it, gets the user from the model and saves the user
in a context object inside the request object. Doing this we are able to access the user from the controllers later on.
Now that we have our API secured, let's create the first endpoints:
router
.route("/projects")
.get(projectsController.getProjects)
.post(projectsController.postProject);
Next step, we need to create our controller :)
const getProjects = (req, res) => {
const { user: currentUser } = req.context;
const projects = ProjectModel.find(currentUser.company);
res.json({projects});
}
const getProjectById = (req, res) => {
const { user: currentUser } = req.context;
const { id: projectId } = req.params;
const project = ProjectModel.findById(projectId, currentUser.company);
if (!project) {
return res.status(401)
}
res.json({project})
};
Simple right? We've just created two functions that will call the model to retrieve the required data. As you can see, we are using the user
from the context to filter the data, so we don't expose projects from other companies.
Let's see the last file, the model:
class Project {
static find(company) {
return PROJECTSDATA
.filter(project => project.company === company)
.map(projectData => new Project(projectData));
}
static findById(id, company) {
const projectData = PROJECTSDATA.find(project => (
project.id === id &&
project.company === company
));
return new Project(projectData)
}
}
Everything looks fine until now, you have the code here. The model just exposes two functions to retrieve the projects, filtering by a company. For the sake of simplicity we save all the projects in PROJECTSDATA
.
So that's it, right? We have an API that exposes the projects from different companies and they are only visible to their employees.
Well, I see a small problem here, developers have to pass down the company
id of the current user from the controllers to the models all the time. Yeah, it is just an argument, but it can create security issues in the future if a developer forgets to filter the projects by the company. Wouldn't it be nice if the model would have access to the context? so the developer just has to do ProjectModel.find()
and the model will be responsible for filtering the data for us. This is what I'll try to solve here.
Getting access to the context
So, the idea is that the model has access to the context, and from here to the current user and his company. The approach I like to take is creating a new set of models for each request, injecting them into the context and injecting the context into the model. I create a new set of models so I make sure that we don't change the context during the execution of one request. If we just add the context to the model at the beginning of the request, whenever a new request starts will update the context for the models, so if the previous request didn't finish it will use a wrong context. With this approach, we keep all the information in the request object.
Let's start, we have to change what the model file is exporting, now we have to export a factory that generates a new model every time, this is as easy as this:
// Before
module.exports = Project;
// Factory
module.exports = () => class Project {
// all the logic goes here
};
Instead of exporting the model, we just export a function that returns the model class, a new one every time we call the function.
Now, we need a new middleware that will inject the model into the context and adds the context into the model, something like this:
const projectFactory = require("../models/project");
module.exports = (req, res, next) => {
const Project = projectFactory();
Project.prototype._context = req.context;
Project._context = req.context;
req.context.models = { Project };
next();
};
We generate a new model for every request, and inject the context in it, both in the class and in the prototype so we have access to it all the time.
Now the model methods don't need to receive the company id through the arguments, so we can remove it and get it from the context:
static find() {
const companyId = this._context.user.company;
const { Project } = this._context.models;
return PROJECTS
.filter(project => project.company === companyId)
.map(projectData => new Project(projectData));
}
static findById(id) {
const companyId = this._context.user.company;
const { Project } = this._context.models;
const projectData PROJECTS.find(project => (
project.id === parseInt(id) &&
project.company === companyId
));
return new Project(projectData);
}
And finally, as we have now the model in the request, our controller doesn't need to require the model anymore, and can get it from the context, and of course, it doesn't need to pass the company to the model anymore!
const getProjects = (req, res) => {
const { Project } = req.context.models;
const projects = Project.find();
res.json({
projects
});
};
const getProjectById = (req, res) => {
const { id: projectId } = req.params;
const { Project } = req.context.models;
const project = Project.findById(projectId);
if (!project) {
return res.status(401).json({
error: "project not found"
})
}
res.json({
project
})
};
From now on, if the model is well implemented, developers don't have to filter the data anymore, and you'll be sure that everything is filtered by the user's company.
This allows us to move some logic to the model, for example, when we need to create a new project, we would use a post request to /projects
, the request just needs to send the name of the project, and the model will insert the user who created it and the company. The controller function would be something like this:
const postProject = (req, res) => {
const { name } = req.body;
const { Project } = req.context.models;
const project = new Project({name});
project.save();
res.json({
project
});
};
And the model save
method would be something like this:
save() {
this.company = this._context.user.company;
this.createdBy = this._context.user.id;
// save to the database
}
This approach can be used not only for models but also for many other functions that need access to the context, for example, a logger function that needs to log the request id.
You can see the repository with all the code and a few more endpoints here
Thanks for reading this post, and please, let me know in the comments what do you think, or if you found a better approach.
Top comments (3)
Hi Fernando,
I agree that adding a global filter data like companyId in each controller would be error-prone and insecure. I like your approach - a middleware which "prepares" models.
I would do some very small changes:
1. I wouldn't directly modify the prototype in the middleware, but create a new model instance:
2. In model there are two classes: Model responsible for fetching/persisting data and a pure Project class to store data:
What do you think?
Hi Kryz! thanks for your comment!
I like your idea! I wasn't sure about modifying the prototype in the middleware, I like your approach, but I think it has to be a better approach because now the static methods are in
ProjectModel
and the instance methods will be inProject
.I think we can export the model like this:
This allows us not to modify the prototype from the middleware and do it in the model. It also binds the whole context to the model, this can be important as we could have more things in the context like a
logger
or any other function that requires the request context.We could also take advantage of the closures and do something like
With this approach, the methods are more elegant but we can't split the model into different files.
What do you think?
Thank you for your answer!
Hi,
I like your idea!
One more thing - the middleware runs the factory on each request, is it good for the application performance?
Maybe it would be better to delay this calls, for example:
and in the controller something like this:
I know, it doesn't look nice, I'm sure there is a better solution..