I'm working on a large CMS system where a particular module and its submodules take advantage of the same backend API. The endpoint is exactly the same for each submodule aside from its "document type".
So a pattern like this is followed:
api/path/v1/{document-type}
api/path/v1/{document-type}/{id}
api/path/v1/{document-type}/{id}/versions
As time goes on the number of modules that use this API grows and I am left with many, many redundant api services that implement 7 CRUD methods:
getAllXs() {...}
getX(id) {...}
getXVersion(id, versionId) {...}
etc...
with an individual method looking like this
getAllXs() {
let endpoint = BASE.URL + ENDPOINTS.X;
let config = ...
return http.get(endpoint, config)
.then(response => response.data);
.catch(...);
}
Where X would be the name of a particular Document Type.
I came to a point where I decided to make a single service and do something like this:
const BASE_URL = window.config.baseUrl + Const.API_ENDPOINT;
const ENDPOINTS = {
"W": "/v1/W/",
"X": "/v1/X/",
"Y": "/v1/Y/",
"Z": "/v1/Z/",
}
getAllDocuments(docType, config={}) {
let endpoint = BASE_URL + ENDPOINTS[docType];
return http.get(endpoint, config)
.then(response => response.data);
.catch(...);
}
...other methods
Where a type is specified and a mapped endpoint is used to build the path.
This reduces all of the document api services down to one. Now this is more concise code wise, but obviously now requires an extra parameter and the terminology is more generic:
getAllXs() --> getAllDocuments()
and it's a bit less 'idiot-proof'. What makes me insecure about the current way it is written is that there are 6 modules that use this API and the same 7 methods in each service.
The questions I keep asking myself are:
Am I bordering anti-pattern with the dynamic functions?
What if I had 10+ modules using the same API?
Your question made me think of a common Object Relational Mapping design problem.
There are no single source of truth when it comes to design, but if your recognize an ORM in what you are building and value object oriented design principles, I have some inspiration for you.
Here's an over simplification of my own vanilla ES6 ORM I have used on many projects (reusing your code snippets for relatability). This deisgn is inspired by heavy ORM frameworks I have used in other languages.
class ORM {
constructor() {
this.BASEURL = window.config.baseUrl + Const.API_ENDPOINT
this.config = {foo:bar} // default config
}
getAll() {
let endpoint = this.BASEURL + this.ENDPOINT
return http.get(endpoint, this.config)
.then(response => response.data)
.catch(...)
}
get(id) {
// ...
}
}
And examples of extension of that class (notice the one with a special configuration)
class XDocuments extends ORM {
static endpoint = '/XDocument/'
constuctor() {
super()
}
otherMethod() {
return 123
}
}
class YDocuments extends ORM {
static endpoint = '/YDocument/'
constuctor() {
super()
}
getAll() {
this.config = {foo:not_bar}
super.getAll()
}
}
Since you are specifically asking if this is bordering anti-patterns. I would suggest reading about SOLID and DRY principles, and ORM designs in general. You will also find code smells about global constants, but this would only be true if you are in a window context. I see you are already on the right path trying to avoid code duplication smell and the shotgun surgery smell. :-)
Good luck and don't hesitate to ask further questions and add additional details in comments!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With