I Spent a Year to Reinvent a Node.js Framework
I spent nearly a year recently to develop a server side TypeScript framework for Node.js during my free time. The framework primarily built for my internal project to help me easily create a restful api using TypeScript.
The framework named Plumier, its inspired by a beautiful flower that grows in front of my room Plumeria. Plumier taken from the name of France botanist Charles Plumier that found Plumeria. I like the name because it’s sounds entirely unique on GitHub and search engine.
Why Another Framework?
I’m not living in a cave, I know there are more than a dozen TypeScript frameworks like this out there in the wild, some of them mature and great and even have LTS version already.
I spent some time to work on some projects using server side TypeScript frameworks, it feels great that we have a lot of choice that is already mature and has great community. But I still have a feeling that there are some technical needs and specifications that make it worth creating another one.
This story will be my subjective opinion about how current frameworks doesn’t fit my needs and how Plumier bring different solution as an alternative.
Reason 1 — Best Practices Inside Framework
If you look at TypeScript frameworks nowadays let say Nest, LoopBack 4, Type Stack Routing Controller etc, you will familiar with their selling words: Dependency Injection, Separation of Concerns, SOLID Principle, etc etc. Almost all of TypeScript framework added best practices from enterprise grade application. I understand that most of TypeScript user comes from C# user, and they may eager hearing those technique also can be used in TypeScript.
Despite I love OO and design pattern and even write My Own IoC Container for my internal projects. But still in my opinion framework should bring more freedom on how programmer layout their code or how they solve their case. The primary focus should be on providing basic functionalities such as validation, data conversion, authorization etc, that make simplest implementation keep simple, robust, secure and fast. Adding design pattern and best practices into a framework is a good selling but make simple implementation no more simple and also make a steep learning curve.
How Does It Implemented on Plumier?
Plumier tends to be an unopinionated framework. The main idea is how to make your work done fast, work correctly and bring freedom on how you solve your case. It follows the spirit of Express for its simplicity, the simplest Plumier application only consist of two parts, a controller and an application startup like example below.
Above code will host a json service at
http://localhost:8000/say/hello?name=<string>&age=<number> receives name and age parameter. This setup showing the minimum framework that need to follow when using Plumier. By default Plumier will look into
./controller directory for controllers, but in this example controller intended to be in the same file with application startup for clarity.
Plumier doesn’t support dependency injection out of the box but it provided an extensible dependency resolver that can be overridden and make it possible to use your favourite dependency injection framework as a custom dependency resolver.
Reason 2 — TypeScript Reflection
TypeScript actually has a powerful reflection features. If you already known decorators you might heard of TypeScript compiler option
From TypeScript perspective, above code contains repetitive code and verbose configuration that can be simplified.
idparameter type is obviously of type of number, it should not need to specifically defined.
- Url parameter
idand method parameter
idhave the same name and bound the same value, Framework should have a way to bind them automatically.
todoparameter is obviously bindable to the request body because of its data type (a class). Framework should have a way to automatically assigned it with request body.
I know LoopBack 4 support many ways to configure the controller, not only using decorators. But in my opinion none giving simpler way since LoopBack 4 using TypeScript as its primary development language.
How Does It Implemented on Plumier?
Plumier built from ground up using dedicated reflection library called tinspector, it make it possible for Plumier to do rich metaprogramming feature such as route generation, parameter binding, static analysis etc that make developing restful api easier, less decorators and cleaner code. Below is previous LoopBack 4 example but in Plumier version.
Plumier uses convention over configuration to generate routes from controller name, method name and parameters name. Thus make Plumier controller cleaner. In the background there are three processes involved to support this features.
- During startup route generator will generate routes based on controller name method decorator and parameters. Based on above controller, route generator will generate
- On each request parameter binder will assign the
idurl parameter into
idparameter of the method using name binding. And then assign request body with
todoparameter of the method using model binding. Note that Plumier has various way to bind parameter with request parameter or request body using some priorities.
- Converter and validator will convert appropriate
idvalue into number and the request body into appropriate
todoparameter data type. If any of the data doesn’t convertible to appropriate type, Plumier will automatically response with http status 400 with informative error message.
Those three processes are Plumier key features that is already activated without specifying any configuration.
Another Plumier feature using the power of TypeScript reflection is static controller analysis during startup. Static analysis means this function analyze controllers to find any problem without having to run the controllers. Using this feature Plumier can give informative error message if found some issue immediately during application startup like picture below
As picture above Plumier has some predefined static analysis to automatically detect issues that possibly causing problem when application running such as: duplicate routes, missing decorators that causing data conversion failed, missing backing parameters on a parameterized route etc. This message important if number of routes generated grows to easier spot the issues and fix them immediately.
Controller inheritance is not a feature in Plumier, instead it just a bonus proof that using proper reflection library will not limit us from using the framework naturally. This behavior currently a problem on the early version of Nest. And not supported on Routing Controller.
Even if controller inheritance is something debatable, it’s nice that Plumier supported it for free. Below is example how you can create base class for a controller and provided basic functionalities there, and inherit them on the derived class.
Above code will generates two routes
POST /cats and
GET /cats/:id both routes will inherit methods from
AnimalsBase class. On the
CatsController class we override the add method manually to inform Plumier about domain data type that will be used by Plumier for validation and data conversion.
Reason 3 — Authorization
Authorization is the most important aspect when developing an API. Once deployed into the cloud you can’t let your data security only depends on client side security, because user should be able to directly access API without having to use UI.
Authorization tends to be complex and tricky, from my experience inside a controller there are more code needed for security than business process or interaction to database. For example for a user registration API we need to specify specific access to the API url like below.
Giving access for Public and Admin is easy but Giving access to Owner or Admin is a little bit tricky and enough to make the controller messy. Another issue is using the same DTO (data transfer object) for each method and role is not secure. Consider a DTO for the POST and PATCH method above is like below.
Code above showing that the DTO contains sensitif data
role that can be modified by all authorized roles including the user itself. Using above DTO will require another logic on the controller to check current login role. It’s a trivial and repetitive task that can make controller look messy.
How Does It Implemented on Plumier?
Plumier has declarative authorization to easily secure rest resources, it also can be overridden to provide a custom authorization. For a simple authorization based on user role, Plumier provided a simple
authorize decorator like example below
Above code is Plumier authorization implementation based on previous requirements described on the table.
POST /users authorized to public and
GET /users?offset&limit authorized to Admin.
As our previous issue on providing authorization for Owner or Admin on
PATCH /users, Plumier provided custom function to easily create custom decorator to authorize a method like below
Above code showing that we created a custom decorator named
authorize.custom function. The implementation is quite simple, we check if current login user role is Admin or if the current user is the owner of the data (the first parameter of the method where the decorator applied match with current login user id).
Using above trick, we able to create reusable solution that can be applied to other method with the same signature. Or we can make more robust implementation to share among controllers by using controller metadata that provided in the callback parameter.
Plumier provided various information inside custom authorization callback parameter such as Koa context, login user, user roles, current controller or method metadata etc that is important to program authorization logic.
Another issue on our previous section is sharing the same DTO for controller methods and roles which cause adding extra logic to check current login role. Plumier provided Domain Authorization to authorize certain user to set value of a domain property like below.
Using above configuration if current login role is not an Admin and sending request containing
role field will cause Plumier automatically return http status 401 (unauthorized) with informative message.
Using declarative authorization is good because it easily can be reviewed from controller or domain, and require less programming effort with cleaner code.
Authorization Analysis Report
Previously mentioned that Plumier has a static controller analysis to show some issues during startup. When authorization module is enabled, static analysis will show extra information about authorization like picture below.
Above analysis enough to easily spot an issue if we misconfigured a method and give wrong access to some roles. This analysis intended to make application security and authorization easily reviewed.
Reason 4 — Middleware Pipeline
Middleware pipeline is process on how middlewares executed one to another using a chain of execution. Most frameworks has their own middleware pipeline logic. This logic mostly affect middleware implementation complexity.
- Middleware doesn’t know when the next middleware finish executed.
- Middleware doesn’t know the result of the next execution.
- Middleware doesn’t know if the next execution successful or not, whether it throws error or not
Due to above limitations create a relatively simple middleware become complex and problematic such as modify previous middleware response from inside middleware or calculating response time need a third party middleware. Express mostly used by TypeScript framework and most of them inherit the same issue.
How Does It Implemented on Plumier?
Plumier has its own middleware pipeline to manage request and ActionResult using stack-like sequence that works like Koa middleware pipeline. Middleware await the execution of the next invocation (can be another middleware or a controller execution) that make it has full control to the next invocation.
Plumier middleware is a TypeScript class implements Middleware interface. Simplest Plumier middleware to proceed the next invocation is like below
Above code showing simplest Plumier middleware, it provided an awaitable invocation object to proceed the next process. Using awaitable invocation object, middleware has full control to the next invocation. It can execute next invocation using some condition such as check for login user or check some query string before proceeding. Further more middleware can modify execution result that will be used to create http response, such as modify the response body, response status etc.
By using Plumier middleware pipeline, creating a middleware in Plumier is relatively simpler. For example for global error handling is like below
Above code just an example to show how you can catch error of the next process using try-catch from inside middleware. Note that Plumier already provided a global error handler so you don’t need to create it yourself. Another example is calculating response time is as easy as below
Above code showing that we print the response time using
console.time to print timestamp easily to the console before and after the next invocation.
Support the Project on GitHub
Framework has their own strength and weakness, it based on design decision. I give my highest respect to all creator and maintainers of frameworks mentioned here for their hard work and great jobs. Plumier born not to compete with them but bring a new and fresh idea as an alternative to easily developing and secure restful api while giving freedom on how programmer solve their problem.
Currently this is the first publication I made for Plumier since it first development a year ago and until now it only has 30 stars on GitHub, most of them comes from my friends. If you keep reading until here and you think you like the framework, please help spread it to the world and you are welcome to go to the project repository and give a star to support it.
It still in BETA 8 now but it stable enough to use in production. There are a lot will be supported in the future such as Open Api and Swager, real time and more, you can check more on the Github issue. Thank you for your time, Im looking back for your contribution in the project.