rest-services
rest-services provides easy to use RESTful API for express.
Build your RESTful API by defining one or more Services with list of Resources.
Installation
Using npm:
$ npm install --save rest-services
What does it look like?
Let's build our first Hello, world! Service. Here is normal usage with ES2015 modules and express:
import express from 'express'
import { RestServices } from 'rest-services'
import ExampleResource from './example-resource'
var app = express();
const config = {
services: [
{
serviceName: "example",
serviceLabel: "Example API",
servicePath: "api",
resources: [
ExampleResource
]
}
]
}
var services = new RestServices(config);
services.mount(app);
var server = app.listen(3000, () => {
var host = server.address().address;
var port = server.address().port;
console.info(`==> 🌎 Listening ${ host } on port ${ port }.`);
});
Following code provides our first Resource:
import { Resource } from 'rest-services'
class ExampleResource extends Resource {
getInitialState() {
return {
resourceId: 'example',
resourceDefinition: {
operations: {
retrieve: {
title: 'Retrieve entity',
description: 'Retrieve entity by id.',
callback: this.retrieveItem.bind(this),
arguments: [
{
name: 'id',
source: {path: 0},
optional: false,
type: 'string',
description: 'Entity id'
}
]
}
},
actions: {},
targetedActions: {}
}
};
}
retrieveItem(args, callback) {
callback(null, {
result: {
"msg": "Hello, world!",
"requestedEntityId": args.id
}
});
}
}
export default ExampleResource;
Now start your server and open your browser with url: http://127.0.0.1:3000/api/example/test.
You will see JSON response from server:
{
msg: "Hello, world!",
requestedEntityId: "test"
}
Documentation
Our example above was one simple use case. Now let's talk about how it really works.
Services
Frst we define Service for our app. In our example we defined Example API Service, which is listening url /api.
You can have multiple services if you like, they all have unique paths.
At the moment all reponses are returned with pure JSON. If you need to alter services response data or format, just extend RestServices class and implement sendResponse(err, req, res, response) method. We will cover this documentation in near future.
Resources
We need to define the Resource(s) to be used with our Service. In our example we defined ExampleResource with id example.
This means that all requests to /api/example will be mapped to our example resource.
You can define three different types of resource mappings:
- operations
- actions and
- targeted actions.
Operations
Supported CRUD +index operations and corresponding HTTP methods are:
- Create (POST)
- Retrieve (GET)
- Update (PUT)
- Delete (DELETE)
- Index (GET)
In our example these would be:
- Create item: POST /api/example
- Retrieve item: GET /api/example/[id]
- Update item: PUT /api/example/[id]
- Delete item: DELETE /api/example/[id]
- Index items: GET /api/example
Actions and targeted actions
Each endpoint can have unlimited number of actions and targeted actions. Both actions are executed by HTTP POST request. The key difference between these two action types is that actions are general, whereas targeted actions are targeted for certain entity id.
- Action (POST)
- Targeted action (POST)
In our example these would be:
- Action: POST /api/example/subscribe
- Targeted action: POST /api/example/[id]/subscribe
Resource parameters
Resource mappings may define list of parameters they are expecting. Parameters are then processed and provided for your callback automatically.
In our example we defined one parameter named id to be retrieved from url path. Parameters can be fetched from url, query string or request payload.
let arguments = [
{
name: 'id',
source: { path: 0 },
optional: false,
type: 'string',
description: 'Entity id from url path, index 0'
},
{
name: 'limit',
source: { param: 'limit' },
optional: true,
type: 'string',
description: 'Limit will be fetched from query string'
},
{
name: 'item',
source: 'data',
optional: false,
type: 'array',
description: 'Entity data from request payload'
}
]
HTTP Access Control (CORS)
If your endpoint will be called from other domain than servers own domain, you need to enable CORS support for preflight requests. You can provide default CORS settings for the whole Service or limit to a single Resource.
Enable CORS by defining key allowedOrigins with list of allowed origins. Following example demonstrates how to allow CORS request from certain domain:
const config = {
services: [
{
serviceName: "example",
serviceLabel: "Example API",
servicePath: "api",
settings: {
cors: {
allowedOrigins: [
"https://www.npmjs.com/package/rest-services"
],
responseHeaders: [
{
key: "Access-Control-Allow-Methods",
value: "POST, GET, OPTIONS, PUT, DELETE"
},
{
key: "Access-Control-Allow-Headers",
value: "Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With"
},
{
key: "Access-Control-Max-Age",
value: "1728000"
},
{
key: "Access-Control-Allow-Credentials",
value: "true"
}
]
}
},
resources: [
...
]
}
]
}
var services = new RestServices(config);
Note that in previous example we also provided a list of response headers to allow better caching of preflight requests.
If you are sure that there is no any security issues with your endpoints, you can allow CORS requests from any domain by giving argument dangerouslyAllowAll: true.
const config = {
services: [
{
serviceName: "example",
serviceLabel: "Example API",
servicePath: "api",
settings: {
cors: {
dangerouslyAllowAll: true
}
},
resources: [
...
]
}
]
}
If there is no reason to allow preflight requests for all resource, you can limit support for only the certain endpoint:
import { Resource } from 'rest-services'
class ExampleResource extends Resource {
getInitialState() {
return {
resourceId: 'example',
cors: {
allowedOrigins: [
"https://www.npmjs.com/package/rest-services"
]
},
resourceDefinition: {
...
}
}
}
}
Security
Note that your API might need additional protection because of XSS. We will cover this documentation in near future.
Complete example
Let's put all pieces in one Resource:
import { Resource } from 'rest-services'
class ExampleResource extends Resource {
getInitialState() {
return {
resourceId: 'example',
resourceDefinition: {
operations: {
retrieve: {
title: 'Retrieve entity',
description: 'Retrieve entity by id.',
callback: this.retrieveItem.bind(this),
arguments: [
{
name: 'id',
source: {path: 0},
optional: false,
type: 'string',
description: 'Entity id'
}
]
},
create: {
title: 'Create entity',
description: 'Create entity.',
callback: this.createItem.bind(this),
arguments: [
{
name: 'entityData',
source: 'data',
optional: false,
type: 'array',
description: 'Entity data'
}
]
},
update: {
title: 'Update entity',
description: 'Update entity.',
callback: this.updateItem.bind(this),
arguments: [
{
name: 'id',
source: {path: 0},
optional: false,
type: 'string',
description: 'Entity id'
},
{
name: 'entityData',
source: 'data',
optional: false,
type: 'array',
description: 'Entity data'
}
]
},
delete: {
title: 'Delete entity',
description: 'Delete entity.',
callback: this.deleteItem.bind(this),
arguments: [
{
name: 'id',
source: {path: 0},
optional: false,
type: 'string',
description: 'Entity id'
}
]
},
index: {
title: 'Index',
description: 'Fetch list of entities based on given criteria',
callback: this.indexList.bind(this),
arguments: [
{
name: 'limit',
source: {param: 'limit'},
optional: true,
type: 'string',
description: 'Limit'
}
]
}
},
actions: {
subscribe: {
title: "Subscribe",
description: "Subscribe to get news from new entities",
callback: this.subscribe.bind(this),
arguments: [
{
name: 'subscription',
source: 'data',
optional: false,
type: 'array',
description: 'Subscription data'
}
]
}
},
targetedActions: {
subscribe: {
title: "Subscribe to entity modifications",
description: "Subscribe to get news from this entity modifications",
callback: this.entitySubscribe.bind(this),
arguments: [
{
name: 'id',
source: {path: 0},
optional: false,
type: 'string',
description: 'Entity id'
},
{
name: 'subscription',
source: 'data',
optional: false,
type: 'array',
description: 'Subscription data'
}
]
}
}
}
};
}
retrieveItem(args, callback) {
let entityId = args.id;
if (isNaN(entityId))
return callback(this.setError(500, "Invalid entity id."));
callback(null, {
result: {
"msg": "Hello, world!",
"requestedEntityId": entityId
}
});
}
createItem(args, callback) {
let entityParams = args.entityData;
callback(null, {
result: false
});
}
updateItem(args, callback) {
callback(null, {
result: false
});
}
deleteItem(args, callback) {
callback(null, {
result: false
});
}
indexList(args, callback) {
let entityIds = [];
callback(null, {
result: entityIds
});
}
subscribe(args, callback) {
let succeed = false;
let entityParams = args.subscription;
callback(null, {
result: succeed
});
}
entitySubscribe(args, callback) {
let succeed = false;
let entityId = args.id;
let entityParams = args.subscription;
callback(null, {
result: succeed
});
}
}
export default ExampleResource;
Test
Run tests using npm:
$ npm run test
Lint
$ npm run lint
Need more infromation?
This module is inspired by Drupal's Services module. Feel free to comment and leave issues.