0.2.0 • Published 5 years ago

rest-services v0.2.0

Weekly downloads
2
License
MIT
Repository
github
Last release
5 years ago

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:

// server.js
import express from 'express'
import { RestServices } from 'rest-services'
import ExampleResource from './example-resource'

var app = express();

// We define our example service with one resource "ExampleResource"
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:

// example-resource.js
import { Resource } from 'rest-services'

// Define your own resources by extending Resource class
class ExampleResource extends Resource {
  /**
  * Define resource endpoints.
  *
  * @return state
  */
  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: {}
      }
    };
  }

  /**
  * Retrieve entity data.
  *
  * @param args value with following keys:
  *   _req  Request object provided by express
  *   _res  Response object provided by express js
  *   ... arguments defined by you, see getInitialState()
  * @param callback
  */
  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: 1) operations 2) actions and 3) 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.

  // Examples how to define arguments with different sources:
  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:

// server.js

// Enable CORS support for service level
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.

// server.js

// Enable CORS for all domains
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:

// example-resource.js
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:

// example-resource.js
import { Resource } from 'rest-services'

// Define your own resources by extending Resource class
class ExampleResource extends Resource {

  /**
  * Define resource endpoints by overriding getInitialState() method.
  *
  * @return state
  */
  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'
              }
            ]
          }
        }
      }
    };
  }

  /**
  * Retrieve entity data.
  *
  * @param args value with following keys:
  *   _req  Request object provided by express
  *   _res  Response object provided by express js
  *   ... arguments defined by you, see getInitialState()
  *
  * @param callback
  */
  retrieveItem(args, callback) {
    // Entity id will be available from args
    let entityId = args.id;

    // Send custom http error codes
    if (isNaN(entityId))
      return callback(this.setError(500, "Invalid entity id."));

    // By default your endpoint will return HTTP status code 200 OK
    callback(null, {
      result: {
        "msg": "Hello, world!",
        "requestedEntityId": entityId
      }
    });
  }

  /**
  * Create entiy.
  *
  * @param args
  * @param callback
  */
  createItem(args, callback) {
    // Request payload data is passed for you
    let entityParams = args.entityData;

    // Implement this functionality...
    callback(null, {
      result: false
    });
  }

  /**
  * Update entity.
  *
  * @param args
  * @param callback
  */
  updateItem(args, callback) {
    // Implement this functionality...
    callback(null, {
      result: false
    });
  }

  /**
  * Delete entity.
  *
  * @param args
  * @param callback
  */
  deleteItem(args, callback) {
    // Implement this functionality...
    callback(null, {
      result: false
    });
  }

  /*
  * Index entities.
  *
  * @param args
  * @param callback
  */
  indexList(args, callback) {
    let entityIds = [];
    // Implement this functionality...
    callback(null, {
      result: entityIds
    });
  }

  /*
  * Subscribe action.
  *
  * @param args
  * @param callback
  */
  subscribe(args, callback) {
    let succeed = false;
    // Request payload data is passed for you
    let entityParams = args.subscription;

    // Implement this functionality...
    callback(null, {
      result: succeed
    });
  }

  /*
  * Entity subscribe, targeted action.
  *
  * @param args
  * @param callback
  */
  entitySubscribe(args, callback) {
    let succeed = false;

    // Entity id will be available from args
    let entityId = args.id;

    // Request payload data is passed for you
    let entityParams = args.subscription;

    // Implement this functionality...
    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.

0.2.0

5 years ago

0.1.16

7 years ago

0.1.15

7 years ago

0.1.14

7 years ago

0.1.13

8 years ago

0.1.11

8 years ago

0.1.10

8 years ago

0.1.9

8 years ago

0.1.8

8 years ago

0.1.7

8 years ago

0.1.6

8 years ago

0.1.5

8 years ago

0.1.4

8 years ago

0.1.3

8 years ago

0.1.2

8 years ago

0.1.1

8 years ago

0.0.8

8 years ago

0.0.7

8 years ago

0.0.6

8 years ago

0.0.5

8 years ago

0.0.4

8 years ago

0.0.3

8 years ago

0.0.2

8 years ago

0.0.1

8 years ago