A module that abstracts the process of consuming a REST endpoint from both client and server side.
npm i restful-model;
const RestService = require('restful-model');
const userService = new RestService('http://example.com/api/v1');
const userModel = userService.registerModel('User', '/users');
// get all users
const users = await userModel.query({view: 'thin'}); // HTTP GET http://example.com/api/v1/users?view=thin
// get user by ID
const user = await userModel.get({id: 1, view: 'full'}); // HTTP GET http://example.com/api/v1/users/1?view=full
// create user
const user = await userModel.create({full_name: "John Doe"}); // HTTP POST http://example.com/api/v1/users
// update user
const user = await userModel.update({id: 1}, {full_name: "John Doe"}); // HTTP PUT http://example.com/api/v1/users/1
// delete user
const result = await userModel.delete({id: 1}); // HTTP DELETE http://example.com/api/v1/users/1
Note: keys supplied in the first arguments for the above methods are used as params in path and query string. To use a param in the path give it the same name as the path placeholder. All other params would be used in query string.
Use RestService model config to configure models
const userService = new RestService('http://example.com/api/v1');
const modelConfig = RestService.modelConfig().customActions({
getFiends: {
method: 'get',
path: '/:id/friends',
}
});
const userModel = userService.registerModel('User', '/users', modelConfig);
const friends = await userModel.getFiends({id: 1, view: 'thin'});
// HTTP GET http://example.com/api/v1/users/1/friends?view=thin
Models can be configured using the RestService.modelConfig() method. This method return an instance of ModelConfig class.
Model id field can be configured using the setIdField method the the ModelConfig class.
const modelConfig = RestService.modelConfig().setIdField('userId'); // configure the name of the ID field
const userService = new RestService('http://example.com/api/v1');
const userModel = userService.registerModel('Users','/users')
NOTE: You have much better and more sophisticated options like GraphQL for querying related models. However, not all services provide the option to consume their data via GraphQL. This means that you might have to write your own resolvers and schemas to consume a service that do not support GraphQL out of the box. In cases where using tools like GraphQL do not meet your requirements, you can use relationship features in this package.
Relationship can be configured using the hasMany and hasOne methods of the ModelConfig class. These 2 methods of the ModelConfig class define model relationship and have the same signature.
Parameter | Description | Type | Default Value |
---|---|---|---|
name(required) | The name of the model | string |
|
fieldName | The name of the field when joining models. Also used as a relation key | string |
|
foreignField(optional) or config | When this argument is a string, it is used as the foreign key of the referenced model. When this argument is an object the next argument is skipped and all setting can be defined in it. Options: - using (string) define the method to call on the referenced model - localField(string) - foreignField(string) - fetchMode(string) (combined|exclusive) default('combined') when combined one request is sent for all entries when exclusive a request is made per entry - params (object) used to define path and query params. Giving a param the same name as a placeholder in the path would inject it in the path. All other params would be used in query string. To use a param from an entry set it value to the name of the target field and prefix it by an @ sign |
string or object |
'id' |
localField(optional) | The local field in the relation. Ignored when foreignField is an object | string |
'id' |
// define services
const articleService = new RestService('http://articles.example.com/api/v1');
const authorService = new RestService('http://authors.example.com/api/v1');
const commentService = new RestService('http:/comments.example.com/api/v1');
// define models
const modelConfig = RestService.modelConfig()
.hasMany('Comments', 'comments', 'articleId')
.hasOne('Author', 'author', 'id', 'authorId');
const articleModel = articleService.registerModel('Article', '/articles', modelConfig);
const authorModel = authorService.registerModel('Author', '/authors');
const commentModel = commentService.registerModel('Comment', '/comments');
// Would get an article model with 2 extra fields author (the fetched author) and comments (array of fetched comments)
const article = await articleModel.get({id: i}, ['author','comments']);
// HTTP GET http://example.com/api/v1/articles/1
// HTTP GET http://example.com/api/v1/authors?id[]=<article.authorId>
// HTTP GET http://example.com/api/v1/comments?articleId[]=<user.id>
// Would get articles. Each item would have model with 2 extra fields author (the fetched author) and comments (array of fetched comments)
const article = await articleModel.query({}, ['author','comments']);
// HTTP GET http://example.com/api/v1/articles
// HTTP GET http://example.com/api/v1/authors?id[]=<article1.authorId>&id[]=<article2.authorId>
// HTTP GET http://example.com/api/v1/comments?articleId[]=<article1.id>&articleId[]=<article2.id>
The examples above use a fetching of related entities with fetchMode of type combined (see). This means that a single request is made to access the related entities of retrieved items. In the above example, a single request is made to retrieve all authors related to all collected articles. This single request is made with all author IDs in a query string. However, this could cause lengthy Request URLs. And since URL have limited size (depends on the server and browser), request will large data set would fail. It is advised to use it only on small data set.
// set up
const articleConfig = RestService.modelConfig()
.hasMany('Media', 'images', {
fetchMode: 'exclusive', // exclusive | combined , exclusive = 1 request per entry, combined 1 request for all entry with query string filter
params: {content_type: 'articles', media_type: 'images', content_id: '@id', size: 'thumb'}
});
const articleModel = articleService.registerModel('Article', '/articles', articleConfig);
const authorModel = authorService.registerModel('Author', '/authors', RestService.modelConfig().hasMany('Article', 'post').hasOne('Media', 'photo', {
fetchMode: 'exclusive',
using: 'get',
params: {content_type: 'authors', media_type: 'images', content_id: '@id', id: '@profilePhotoId', size: 'medium'}
}));
const media = mediaService.registerModel('Media', '/:content_type/:content_id/media/:media_type');
const author = await authorModel.get({id: 107}, ['photo']);
// HTTP GET http://authors.example.com/api/v1/authors/107
// HTTP GET http://media.example.com/api/v1/authors/107/media/images/<author.profilePhotoId>
const article = await articleModel.get({id: 1}, ['images']);
// HTTP GET http://authors.example.com/api/v1/authors/107
// HTTP GET http://media.example.com/api/v1/articles/1/media/images
const authors = await authorModel.query({}, ['photo']);
// One request for all authors
// HTTP GET http://authors.example.com/api/v1/authors/107
// One request for image per author
// HTTP GET http://media.example.com/api/v1/authors/<author1.id>/media/images/<author1.profilePhotoId>
// HTTP GET http://media.example.com/api/v1/authors/<author2.id>/media/images/<author2.profilePhotoId>
// ....
const article = await articleModel.query({}, ['images']);
// One request for all articles
// HTTP GET http://articles.example.com/api/v1/articles
// One request for images per articles
// HTTP GET http://media.example.com/api/v1/articles/<article1.id>/media/images
// HTTP GET http://media.example.com/api/v1/articles/<article2.id>/media/images
// ....
A request is made for each item. Consider using caching where relevant.
Middlewares are used to process server requests. When defining middlewares the order of middlewares is important:
There are two default middlewares, a Request middleware and a Response middleware.When using the default middlewares, processing a request has the following steps
Middleware function arguments
Parameter | Type | Description |
---|---|---|
input | mixed |
The input value would depend on the preceding middleware. It can be a request options a response or anything else. The first middleware always receives the request options. |
next | function |
The callback used to deliver the middleware result to the next middleware. |
resolve | function |
The resolver callback. This callback can be used to skip the rest of the middlewares and resolve the request with response data. |
context | object |
Context object for sharing data with middlewares. |
Aside the arguments listed above, other arguments can be passed to the following middleware by calling next() with additional arguments. The additional arguments would be appended the argument list of the next middleware function.
function(input, next, resolve, context, ...extraArgs) {
const result = process(input); //.... do something with input
if(/*some condition*/){
next(result, ...extraArgs)// pass result to next middleware
} else {
resolve(result);// exit middleware chain with result
}
}
const RestService = require('restful-model');
const {fetchRequest, fetchResponse} = RestService.defaultMiddlewares;
const userService = new RestService('http://example.com/api/v1');
async function addBasicAuthHeader (httpRequestOptions, next) {
const {username, password} = await getAuthData(/*...*/);// imaginary function
const hash = base64Encode(`${username}:${password}`); // imaginary function
// add authorisation header
httpRequestOptions.headers['Authorization'] = `Basic ${hash}`;
// pass request options to the next middleware (fetchRequest)
next(httpRequestOptions);
}
userService.useMiddlewares([addBasicAuthHeader, fetchRequest, fetchResponse]);
Setup
const RestService = require('restful-model');
const {OAuth} = require('oauth');
const {fetchRequest, fetchResponse} = RestService.defaultMiddlewares;
const userService = new RestService('http://example.com/api/v1');
const oauthClient = new OAuth(
'http://some.3rd.party.api/api/v1',
'http://example.com/api/access_token',
clientID,
clientSecret,
'1.0',
callbackURL,
'HMAC-SHA1'
);
async function addOAuthHeader (httpRequestOptions, next, resolve, {request: requestContext}) {
const {accessToken, accessTokenSecret} = requestContext;
const authHeader = oauthClient.authHeader(input.url, accessToken, accessTokenSecret, input.method)
// add authorisation header
httpRequestOptions.headers['Authorization'] = authHeader;
// pass request options to the next middleware (fetchRequest)
next(httpRequestOptions);
}
userService.useMiddlewares([addBasicAuthHeader, fetchRequest, fetchResponse]);
Now make request and pass request context
const {accessToken, accessTokenSecret} = await getUserToken(/*...*/)// imaginary function
// In the last argument we pass access token data to middlewares
let users = await userModel.query({view: 'thin'}, [], {accessToken, accessTokenSecret});
const userService = new RestService('http://example.com/api/v1');
const userModel = userService.registerModel('User', '/users');
let cache = {};// very minimal cache. Ideally a more sophistictated cache should be used.
const requestHandler = function requestHandler(input, next, resolve) {
if (cache[input.url]) { // use caches
resolve(cache[input.url]);
} else { // continure with the request
const response = await makeRequest(input);// imaginary function
next(response, input);// passes the original input. the next middleware would receive it as extra argument
}
};
const responseHandler = function responseHandler(input, next, resolve, originalInput) {
if(originalIpunt.method === 'GET') {
cache[originalInput.url] = input;// save only get request
} else if(originalIpunt.method !== 'GET' && cache[originalInput.url]) { // PUT, DELETE, POST
delete cache[originalInput.url]; delete GET path if model was updated
}
next(input);
};
userService.useMiddlewares([requestHandler, responseHandler]);
// get all users - this call goes to the server
let users = await userModel.query({view: 'thin'}); // HTTP GET http://example.com/api/v1/users?view=thin
// get all users - the next call does not. Data is fetched from cache
users = await userModel.query({view: 'thin'}); // HTTP GET http://example.com/api/v1/users?view=thin
// get all users - this call goes to the server since a different param was passed
users = await userModel.query({view: 'full'}); // HTTP GET http://example.com/api/v1/users?view=thin