I recenlty had the opportunity to use OOP patterns on Nodejs environment, and I had a big time doing so.
Lets see what we’re gonna build this time:
Model raw format as is stored on database is as follows:
developer
{"id": 23,"name": "AresGalaxy","url": "https://aresgalaxy.io/"},
app
{"id": 21824,"developer_id": 23,"title": "Ares","version": "2.4.0","url": "http://ares.en.softonic.com","short_description": "Fast and unlimited P2P file sharing","license": "Free (GPL)","thumbnail": "https://screenshots.en.sftcdn.net/en/scrn/21000/21824/ares-14-100x100.png","rating": 8,"total_downloads": "4741260","compatible": ["Windows 2000","Windows XP","Windows Vista","Windows 7","Windows 8"]},
When fetching developers resource, it should remain as it is. But on fetching apps we need to merge developer
model like this:
{"id": 21824,"developer_id": 23,"author_info": {"id": 23,"name": "AresGalaxy","url": "https://aresgalaxy.io/"},"title": "Ares","version": "2.4.0","url": "http://ares.en.softonic.com","short_description": "Fast and unlimited P2P file sharing","license": "Free (GPL)","thumbnail": "https://screenshots.en.sftcdn.net/en/scrn/21000/21824/ares-14-100x100.png","rating": 8,"total_downloads": "4741260","compatible": ["Windows 2000","Windows XP","Windows Vista","Windows 7","Windows 8"]},
So here are my thoughts on this:
We need to declare resources in a very straightforward manner, but it seems like every resource may be different, both in format and output.
So we need to extract “common” parts from Resource
concept and build different and independent implementations for each Model
.
What’s a Model
? On REST paradigm we usually call Resource to some domain item
that is represented through an URL (api.io/rest/employee
), we can easily interact with it using HTTP verbs and providing several parameters.
When writing maintainable APIs we need to differenciate from code which describes rules for every resource and code which defines how HTTP connections are fulfilled.
So I end up by creating two basic entities which are models
and resources
.
So we have two models classes developer
and app
and a single resource
class. But, on runtime we’ve two resource instances, each of those has its model instance which is in charge of the specific domain rules.
So this is the start script:
const { setConfig } = require("ritley");setConfig(require("./ritley.conf"));
const BasicResource = require("./resources/basic-resource");
[require("./models/app"),require("./models/developer"),].forEach(Model => new BasicResource(new Model));
We’re using ritley. A lightweight package I made a month ago for fast backend development and specifically REST services.
So in the previous code we only require our ritley configuration which basically setups rest path, static assets folder (if needed) and the port to listen.
Then we just loop over models, and create a resource instance to be tied with its model and we’re up and ready.
Lets take a look over folder structure:
.├── adapters│ ├── low.conf.js│ ├── low.js│ └── low-provider.js├── low.database.json├── models│ ├── app.js│ ├── common.js│ └── developer.js├── package.json├── README.md├── resources│ └── basic-resource.js├── ritley.conf.js├── run.js├── **test**│ └── developers.test.js
4 directories, 13 files
We’ve created models/common.js abstract class to be a starting point for further models:
const { inject, createClass } = require("kaop")const LowProvider = require("../adapters/low-provider");
module.exports = CommonModel = createClass({adapter: null,constructor: [inject.args(LowProvider), function(_db) {this.adapter = _db;}], read() {return new Promise(resolve => resolve("read not implemented"));},find() {return new Promise(resolve => resolve("find not implemented"));},toString(obj) {return JSON.stringify(obj);}})
You may noticed that I’m not using harmony ES classes. Thats because we need something like decorators and we don’t want to use any code transformer for now. Instead we’re using kaop to easily allow reflection techniques such as Dependency Injection.
So basically previous code declares an abstract model that will contain a lowdb instance adapter to access database. If we change our database service we only have to care about importing another provider.
Code below represents models/developer.js:
const { extend } = require("kaop");const CommonModel = require("./common");
module.exports = DeveloperModel = extend(CommonModel, {path: "developer", read() {return new Promise(resolve =>resolve(this.adapter.getCollection("developers")));}});
This only differs from common model on read method implementation, so we just replace it with a new one.
Note that our DeveloperModel contains path property which will be used by basic resource to listen several paths. Here is how:
const { extend, override } = require("kaop");
module.exports = BasicResource = extend(AbstractResource, {constructor: [override.implement, function(parent, _model) {parent(_model.path);this.model = _model;}],
get(request, response) {let prom = null;
if(request.query.id) {
prom = this.model.find(request.query);
} else {
prom = this.model.read();
}
prom.then(result =>
this.writeResponse(response, this.model.toString(result)));
},
writeResponse(response, body) {body && response.write(body);response.statusCode = 200;response.end();}})
BasicResource extends from AbstractResource overriding its constructor for providing the path as you can see on highlighted line, which will be invoked for each instance. As we saw on the start script, models are passed down to resources to properly build our HTTP listeners. BasicResource’s get method will intercept all HTTP GET requests pointing to each path. One instance which was configured with developer model will effectively listen only on <host>/rest/developer path and so forth.
So, a client requesting <host>/rest/developer will be answered by BasicResource instance which was created with DeveloperModel instance.
For instance if we want to allow POST or PUT requests we need to write down a post method on BasicResource, ritley allow us to simply write methods named as HTTP verbs, so any requests that matches will be handled. If we need to allow POST only on several paths we may need to extend BasicResource into AdvancedResource or something which allows more HTTP verbs. This is best practices to properly separate concerns.
And perhaps models need to be grouped by what kind of resource they need to be mounted on.
For example:
const { setConfig } = require("ritley");setConfig(require("./ritley.conf"));
const BasicResource = require("./resources/basic-resource");const AdvancedResource = require("./resources/advanced-resource");
[require("./models/app"),require("./models/developer"),].forEach(Model => new BasicResource(new Model));
[require("./models/company")].forEach(Model => new AdvancedResource(new Model));
Now lets take a look over initial requirements to see if this is a good approach (question — answer):
**adapter**
folder, we’re using an awesome resource such as lowdb. We have 3 different files: **low.conf.js**
which contains database path, **low.js**
which wraps lowdb methods into domain related actions for models to consume and **low-provider.js**
which declares a singleton dependency for injecting into models so we can rapidly switch over different database services :)**adapters/low.js**
:
**getMappedCollection(uid, joinuid, joinkey, newkey) {** const joincollection = this.instance.get(joinuid);return this.instance.get(uid).map(app => this.mergePredicate(app,joincollection.find({ "id": app[joinkey]}),newkey)).value();},mergePredicate(app, subject, newkey) {return { ...app, { [newkey]: ...subject } };},
and then, since app model is the only one who provides nested items we make use of it **models/app.js**
:
read() {return new Promise(resolve =>resolve(this.adapter.getMappedCollection( "apps", "developers", "developer_id", "author_info")));},
const { extend, override } = require("kaop");const BasicResource = require("./basic-resource");
// we only need to implement a new method since this class inherits// from BasicResourcemodule.exports = AdvancedResource = extend(BasicResource, {
post(request, response) {// create entry logic}});
**toString()**
method from **models/common.js**
. Say that DeveloperModel needs to output on XML format because some of our partners is still working with 2008 SQL Server so far..
const { extend } = require("kaop");const CommonModel = require("./common");const xmlconverter = require("awesomexmlparser");
module.exports = DeveloperModel = extend(CommonModel, {path: "developer",read() {return new Promise(resolve =>resolve(this.adapter.getCollection("developers")));},toString(obj) {return xmlconverter.stringify(obj);}})
You can check the code here https://github.com/k1r0s/micro-ritley-lowdb-example