npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rest-services

v0.2.0

Published

RESTful services for Express.js.

Downloads

158

Readme

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.