/express-rested

Manage resources with REST through Express

Primary LanguageJavaScriptMIT LicenseMIT

express-rested

REST is a great way to create an HTTP API to manage resources. It's however a poor API to do rights management on top on. Rights management is often based on CRUD (Create, Read, Update, Delete). A REST PUT operation however can mean either create or update, depending on the resource already existing or not. The REST rules are simple to follow, but when adding any logic, can be a bit of a pain. This module helps you get around that.

In essence:

  • This module does not require you to set up any routes to manage your resources, it's all done for you.
  • You have full control over rights management (CRUD style).
  • You have full control over how (if) data should persist.
  • Any object can be a resource, you register its constructor (or ES6 class).
  • The resource classes you define can be used in browser as well as in Node.js, so you can write universal JavaScript.
  • Resources are always sent back to the client in JSON format.
  • In URLs, you may refer to resources with or without .json extension.

Installation

npm install --save express-rested

Usage

Given a resource "Beer"

resources/Beer.js

class Beer() {
	constructor(id, info) {
		this.id = id;
		this.edit(info);
	}

	createId() {
		this.id = this.name;
		return this.id;
	}

	edit(info) {
		this.name = info.name;
		this.rating = info.rating;
	}
}

module.exports = Beer;

Examples

Base example

const app = require('express')();
const rest = require('express-rested')(app);

rest.add(require('./resources/Beer'), '/rest/beers');

app.listen(3000);

Persisting data

const beers = rest.add(require('./resources/Beer'), '/rest/beers');

beers.loadMap(require('./db/beers.json'));

beers.persist(function (ids, cb) {
	fs.writeFile('./db/beers.json', JSON.stringify(this), cb);
});

Rights management

rest.add(require('./resources/Beer'), '/rest/beers', {
	rights: {
		read: true,     // anybody can read
		delete: false,  // nobody can delete
		create: function (req, res, resource) {
			return res.locals.isAdmin;  // admins can create
		},
		update: function (req, res, resource) {
			return res.locals.isAdmin;  // admins can update
		}
	}
});

Using an Express sub-router

const express = require('express');
const app = express();
const router = new express.Router();
const rest = require('express-rested')(router);

app.use('/rest', router);

// not specifying a path means the collection path will become /rest/Beer

rest.add(require('./resources/Beer'));

Supported REST calls

GET POST PUT DELETE
/Beer Returns all beers Creates a new beer Sets the entire beer collection Deletes all beers
/Beer/123 Returns a beer Not supported Creates or updates a beer Deletes a beer

API

Resource types can be declared as any class or constructor function. There are a few APIs however that you must or may implement for things to work.

Resource API

Your resource class may expose the following APIs:

constructor(string|null id, Object info)

This allows you to load objects into the collection. During a POST, the id will be null, as it will be assigned at a later time using createId() (see below). If the data in info is not what it's supposed to be, you may throw an error to bail out.

Required for HTTP methods: POST, PUT.

edit(Object info) (optional)

This enables updating of the resource value. The info argument is like the one in the constructor. If the data in info is not what it's supposed to be, you may throw an error to bail out.

Required for HTTP method: PUT

createId() -> string (optional)

Should always return an ID that is fairly unique. It could be a UUID, but a username on a User resource would also be perfectly fine. It's not the resource's job to ensure uniqueness. ID collisions will be handled gracefully by express-rested. The createId() method must store the ID it generates and returns.

Required for HTTP method: POST

Notes

No other requirements exist on your resource. That also means that the ID used does not necessarily have to be stored in an id property. It may be called anything. Express-rested will never interact with your resource instances beyond:

  • reading the Class/constructor name (when auto-generating URL paths)
  • Calling the constructor and methods mentioned above

Rest library API

Importing the library:

const rested = require('express-rested');

This imports the library itself.

const rest = rested([express.Router restRouter]);

Instantiates a rest object on which you can create collections for resources.

Make sure that the express router you want to use has the JSON body parser enabled. Else we won't be able to receive data. Also, ensure that it listens for incoming requests on a reasonable base URL (such as /rest). The URLs to our collections will sit on top of this. The router is optional, but as you can imagine it hardly makes sense to use this library without having it register HTTP routes.

rest.add(constructor Class[, string path, Object options]) -> Collection

Creates and returns a collection for objects of type Class. If you have set up an Express Router, all routes to this collection will automatically be registered on it. The path you provide will be the sub-path on which all routes are registered. For example the path /beer will sit on top of the base path (eg: /rest) and will therefore respond to HTTP requests to the full route that is /rest/beer. If you do not provide a path, the name of the class you provide will be used (and prefixed with /, eg.: '/Beer').

Options (all optional):

  • rights: an object, boolean or function that will be applied to all CRUD operations (read on for the applied logic).
  • rights.create: a boolean or a function(req, res, resource) that returns a boolean indicating whether or not creation of this resource may occur.
  • rights.read: a boolean or a function(req, res, resource) that returns a boolean indicating whether or not reading of this resource may occur.
  • rights.update: a boolean or a function(req, res, resource) that returns a boolean indicating whether or not updating of this resource may occur.
  • rights.delete: a boolean or a function(req, res, resource) that returns a boolean indicating whether or not deletion of this resource may occur.
  • persist: a function that will be called after each modification of the collection. See the documentation on persist below for more information on the function signature.

Resource collection API

collection.loadMap(Object map)

Fills up the collection with all objects in the map. The key in the map will be used as the ID. For each object, new Class(key, object) will be called to instantiate the resource.

collection.loadOne(string id, Object info)

This instantiates a resource object from info and loads it into the collection.

collection.has(id) -> boolean

Returns true if the collection has a resource for the given id, false otherwise.

collection.get(id) -> Class|undefined

Returns the resource with the given id it it exists, undefined otherwise.

collection.getIds() -> string[]

Returns all IDs in the collection.

collection.getMap() -> Object

Returns a copy of the complete map of all resources.

collection.getList() -> Class[]

Returns all resources as an array.

collection.set(string id, Class resource, Function cb)

Ensures inclusion of the given resource into the collection. Triggers the persist callback.

collection.setAll(Class resources[], Function cb)

Deletes all resources not given, and creates or updates all resources given in the resources array. Triggers the persist callback.

collection.del(string id, Function cb)

Deletes a single resource from the collection. Triggers the persist callback.

collection.delAll(Function cb)

Empties the entire collection. Triggers the persist callback.

collection.persist(function (string ids[], [Function cb]) { })

Registers a function that will be called on any change to the collection, and is passed an array of IDs that were affected. If you pass a callback, you will have a chance to do asynchronous operations and return an error on failure. This error will find its way to the client as an Internal Service Error (500). If you don't pass a callback, you may still throw an exception to achieve the same.

License

MIT