The “Service Stack” pattern is designed to enable high reusability in Node JS API projects that interact with a data store. This pattern is not intended to replace proper Node JS modules, but instead provide a clean approach to sharing business logic across services within a project.
The pattern consists of three clearly defined layers:
The service layer will define the service end points or routes that will make up the API. The service class responsibilities include input validation, security, input parsing, response handling and coordination with external APIs. The service layer is specifically designed to be built in a web framework such as Express JS, HapiJS, or KoaJS. It is designed to be used by any consumer of a rest API such as HTML5 applications, mobile apps, or other services.
The coordinator layer contains all business logic and is designed to be executed by the service and other libraries including stand-alone applications. This layer should be designed to operate like a library without the need to simulate components of a web based library.
The model layer contains all data logic, data validation and interactions with the underlying data store. The model layer should be designed to shield the coordinator from the details of the underlying data store.
In a basic scenario where the logic flows through a single service stack, the request is handled by the service calling one or more functions on the coordinator which proceeds to call one or more functions on the model.
Referring to the diagram above:
- A request is received by the service where the permissions are verified.
- The input data is validated to check for required fields and malicious content.
- The coordinator function(s) is called.
- The service is required to parse the input data and pass it to the coordinator function(s) is the required format.
- The coordinator will then execute any business logic and call the model.
- In the instance of a CRUD application, there may be no business logic required so a simple pass through will be acceptable.
- The model will take the input and perform data validation prior to making any modifications. Additionally some validation may occur during a data fetch request to ensure that valid search values have been provided.
- Once the action is complete, the model is responsible for formatting the data and returning it to the coordinator where additional business logic may executed.
- The service will process the results further and form a response.
The basic scenario is fine for most CRUD applications, but advanced applications will have shared concerns. One service may need to access data maintained by another service to perform validation. In this instance, the components can then be used by the other service components.
In a complex usage scenario, the controller will call multiple coordinators in order to achieve the proper results. A coordinator may also call multiple models to achieve results. Being that this can occur, strong design of the coordinator and model functions should be performed to enhance the reusability of the layers.
When designing the coordinator and model components, there are some considerations that must be taken into account.
- Does the service stack need to perform low latency tasks?
- Is there stateful data that the service, coordinator and model all need to share?
- Are the components of the service stack reusable?
Classes are useful when stateful data is to be shared between the service, coordinator and model layers. The coordinator and model should be implemented as classes and the stateful data should be passed in through the constructor or other injector function.
Note that when using classes, a new class hierarchy can be instantiated with each request and the garbage collector must clean this up after each call which may result in slower performance in a low latency application when the system is under load.
Singletons are useful in low latency services because there is no need to instantiate a class hierarchy for each request. Having the coordinator and model layers implement singletons will eliminate the excess heap garbage that is generated by classes. Stateful information can be passed to the coordinator and model layers by adding additional parameters to functions.
Object factories may be used with classes to provide pooling of resources and reduce the amount of garbage collection that must be performed. Stateful data can be transferred to the class hierarchy by adding an injector function rather than using the constructor. Either have the factory function pass the data or the calling object can inject the data once the classes have been retrieved. New classes would only be created when the pool is empty and limits may be created for how many objects may be stored at any one time.
JSON schema should be used to perform the validations used through out the pattern. By using JSON schema, the same validations present in the API can then be shared with 3rd party users such as other APIs or UIs. The tv4 library provides JSON schema validation up to the latest specification.
It is recommended that promise chains are used across all three layers to facilitate readability and code reuse. Promise provide a nice chain of calls that can be easily read while the callback method becomes harder to understand the deeper the call chain grows.
Popular libraries include Q and Bluebird as well as native promises.