- Build a CRUD where the user can register companies, units, assets and users.
Role-Specific Authentication
Input Data Strict Validation
Global Error Handling
Complete Database Abstraction
Automatic Document Archivation
MongoDB Entities Association
Environment Variables
MongoDB Associations and Database Integrity
Archive Search
User Company Matching
Clean Code / Clean Architecture
- NodeJS
- Express
- Typescript
- JsonWebToken
- Zod
- BCrypt
- Mongoose
- Docker / Docker-Compose (MongoDB and Mongo Express)
This is a REST API, built using the Model Service Controller (MSC) architecture.
Let's put it this way. Think of the MVC architecture. Now insert a Service layer that will be responsible for enforcing the company's business rules. We now have an architecture known as MVCS. Well, MVCS is for Full Stack applications and this is a Back-End API, so we have to get rid of the View (V) part. Rearrange the letters and we are left with MSC: A Back-End focused architecture, that separates the layers between Controller, Service and Model, each one having a very well defined responsibility.
Keeping it short, the Controller receives requests, extracts data and responds with the requested information. The Service enforces business rules and validations, while helping with data integrity maintenance. The Model layer is the only one that should send queries and retrieve information from the database.
The features listed below are already working, although some of them could be improved. I talk more about this in the Final Observations section of this README.
Users have roles assigned to them. It dictates what they can and cannot do in the API. The "user" role has limited access to information and can edit very few things. Users with "manager" role can create, delete and edit assets and units. New companies and new users can only be created by the "admin" role. There are more details about this in the Endpoints section.
Incoming data is intercepted and has to have the correct properties and types in order to go through, or else a validation error is thrown. Objects that contain extra properties are also rejected, even if they contain the necessary information in the right types.
This is done with the 'express-async-errors' package. It encapsulates the whole application and intercepts all thrown errors, which are then sent to the errorHandler middleware. This is a very organized way of treating errors and standardizing requests output, making error messages more accessible and the lives of both users and developers a lot easier.
The Model layer is built in a way that completely abstracts MongoDB from the rest of the API. As long as it's classes correctly implement the Model interfaces, everything should keep working just fine, even after a database migration to a completely different one.
Its not always a good thing to lose data forever, even when its intentional.
In this API, when a document is deleted from it's original collection its then saved in a Archive collection, where it will no longer be available for consultation by common requests. This way, old data is not lost forever and can be consulted by database admins should it become necessary.
For testing this, follow the Running the Application Locally instructions, create and delete some documents, then go to http://localhost:8081/ to open Mongo Express.
The username and password are both "admin".
Open the TractianDB database. You will find an "archives" collection, where you have access to all deleted documents.
Mongoose has a powerfull functionality that allows us to very easily associate documents from different collections. This enables the populate method, where you pass the fields names and all associated information from other collections will be retrieved. Concatenating populate("company") on the User model brings all the details from the companies the users work at.
You can also populate multiple fields at the same time. The Company model is associated with User, Asset and Unit models. Calling populate("employees units assets") retrieves all information pertinent to those collections on a Company query.
Method | URL | Role |
---|---|---|
POST | http://localhost:3001/users/login | none |
POST | http://localhost:3001/users | admin |
GET | http://localhost:3001/users | manager |
GET | http://localhost:3001/users/:id | manager |
PATCH | http://localhost:3001/users/:id/role/:newRole | admin |
PUT | http://localhost:3001/users/:id | admin |
DELETE | http://localhost:3001/users/:id | admin |
Method | URL | Role |
---|---|---|
POST | http://localhost:3001/assets | manager |
GET | http://localhost:3001/assets | user |
GET | http://localhost:3001/assets/:id | user |
GET | http://localhost:3001/assets/:id/status | user |
GET | http://localhost:3001/assets/:id/health | user |
PATCH | http://localhost:3001/assets/:id/health/:newHealth | user |
PATCH | http://localhost:3001/assets/:id/status/:newStatus | user |
PUT | http://localhost:3001/assets/:id | manager |
DELETE | http://localhost:3001/assets/:id | manager |
Method | URL | Role |
---|---|---|
POST | http://localhost:3001/units | manager |
GET | http://localhost:3001/units | manager |
GET | http://localhost:3001/units/:id | manager |
PATCH | http://localhost:3001/units/:id/assets/add/:assetId | manager |
PATCH | http://localhost:3001/units/:id/assets/remove/:assetId | manager |
PUT | http://localhost:3001/units/:id | manager |
DELETE | http://localhost:3001/units/:id | manager |
Method | URL | Role |
---|---|---|
POST | http://localhost:3001/companies | admin |
GET | http://localhost:3001/companies | admin |
GET | http://localhost:3001/companies/:id | admin |
GET | http://localhost:3001/companies/:id/employees | manager |
GET | http://localhost:3001/companies/:id/assets | manager |
GET | http://localhost:3001/companies/:id/units | manager |
PATCH | http://localhost:3001/companies/:id/employees/add/:employeeId | manager |
PATCH | http://localhost:3001/companies/:id/employees/remove/:employeeId | manager |
PATCH | http://localhost:3001/companies/:id/assets/add/:assetId | manager |
PATCH | http://localhost:3001/companies/:id/assets/remove/:assetId | manager |
PATCH | http://localhost:3001/companies/:id/units/add/:unitId | manager |
PATCH | http://localhost:3001/companies/:id/units/remove/:unitId | manager |
PUT | http://localhost:3001/companies/:id | admin |
DELETE | http://localhost:3001/companies/:id | admin |
Make sure you have Node, Docker and docker-compose installed. Then access the root directory of the project through the terminal and execute the following commands.
- Install the dependencies:
npm i
- To install the docker containers:
npm run compose:up
- To populate the database:
npm run db:seed
- To run the application:
npm start
- To remove the docker containers when you are done:
npm run compose:down
Now you can test the application with a client of your liking. Login with email and password of one of the users below and generate a token for testing. Use the POST /users/login route with a body containing the login info.
User | Password | Role | |
---|---|---|---|
Juvenal | juvenal@tractian.com | data-grandmaster | admin |
João das Neves | joao@freiossupremos.com | sabe-nada | user |
Emerson | emerson@freiossupremos.com | emersinho-gerente | manager |
Roberta | roberta@freiossupremos.com | robertinha-gerente | manager |
I am aware of many things I could have done to make this API a lot better, improving its maintainability and integrity, but some of these things would have to be implemented more carefully in order to avoid unnecessary complexity and dependency cycles. Unfortunately, there was not enough time. I listed some of those things below.
Please, note that I actually have the knowledge on how to implement most of these features.
Those should never be stored somewhere with easy access, as it could pose a serious threat to the company. This repository is just for demonstration purposes, so having the .env file publicly accessible is fine.
It is necessary to make verifications during database manipulation to maintain its integrity and organization.
In the current state, users can add non-existing ids as references for association, and it would go through without any resistance, as long as they are valid instances of the MongoDB ObjectId.
The same goes for documents deletion. If a deleted document is referenced by another entity, this entity will still reference it. It would be ideal to completely disassociate it from other documents.
It's possible to create a router and requests for searching deleted documents. It would be even easier than it was doing it for the main database entities, as there would be very little or no manipulation whatsoever.
Users should be able to only create and manipulate documents of their own companies. As it is now, a manager of company A could create an asset or unit assigning company B as owner. A user of company C could change status and health from assets of company A. And so on.
The solution is quite simple. All users carry the the id of the companies they work for. The company id, as well as the user's id and role are stored as payload in the token when logged in, so its only necessary to extract them upon authentication, store them in the request headers and send it forward. After that, a couple 'ifs' on each route would be enough to check the user's company and grant or deny permission based on what they are trying to do. Having this data available would even make it unnecessary to pass the company id through the request route.
With more time available, I'd consider this to be an easy thing to implement and to be of great value to the API and database integrity.
This was considered a diferential for this challenge. I did some research and started trying to implement it, but it got confusing and messy very quickly. For this reason I decided to stick to what I know.
As Uncle Bob says himself in his books, this is not meant for new developers.