Home

Awesome

Restivus v0.8.12 Build Status

REST APIs for the Best of Us!

Restivus makes building REST APIs in Meteor 0.9.0+ easier than ever before! The package is inspired by RestStop2 and Collection API, and is built on top of Simple JSON Routes to provide:

Version 0.8.0 Released!

Table of Contents

Getting Started

Installation

You can install Restivus using Meteor's package manager:

> meteor add nimble:restivus

Quick Start

Often, the easiest way to explain something is by example, so here's a short example of what it's like to create an API with Restivus (keep scrolling for a JavaScript version):

CoffeeScript:
Items = new Mongo.Collection 'items'
Articles = new Mongo.Collection 'articles'

# Restivus is only available on the server!
if Meteor.isServer

  # Global API configuration
  Api = new Restivus
    useDefaultAuth: true
    prettyJson: true

  # Generates: GET, POST on /api/items and GET, PUT, PATCH, DELETE on
  # /api/items/:id for the Items collection
  Api.addCollection Items

  # Generates: POST on /api/users and GET, DELETE /api/users/:id for
  # Meteor.users collection
  Api.addCollection Meteor.users,
    excludedEndpoints: ['getAll', 'put']
    routeOptions:
      authRequired: true
    endpoints:
      post:
        authRequired: false
      delete:
        roleRequired: 'admin'

  # Maps to: /api/articles/:id
  Api.addRoute 'articles/:id', authRequired: true,
    get: ->
      Articles.findOne @urlParams.id
    delete:
      roleRequired: ['author', 'admin']
      action: ->
        if Articles.remove @urlParams.id
          status: 'success', data: message: 'Article removed'
        else
          statusCode: 404
          body: status: 'fail', message: 'Article not found'
JavaScript:
Items = new Mongo.Collection('items');
Articles = new Mongo.Collection('articles');

if (Meteor.isServer) {

  // Global API configuration
  var Api = new Restivus({
    useDefaultAuth: true,
    prettyJson: true
  });

  // Generates: GET, POST on /api/items and GET, PUT, PATCH, DELETE on
  // /api/items/:id for the Items collection
  Api.addCollection(Items);

  // Generates: POST on /api/users and GET, DELETE /api/users/:id for
  // Meteor.users collection
  Api.addCollection(Meteor.users, {
    excludedEndpoints: ['getAll', 'put'],
    routeOptions: {
      authRequired: true
    },
    endpoints: {
      post: {
        authRequired: false
      },
      delete: {
        roleRequired: 'admin'
      }
    }
  });

  // Maps to: /api/articles/:id
  Api.addRoute('articles/:id', {authRequired: true}, {
    get: function () {
      return Articles.findOne(this.urlParams.id);
    },
    delete: {
      roleRequired: ['author', 'admin'],
      action: function () {
        if (Articles.remove(this.urlParams.id)) {
          return {status: 'success', data: {message: 'Article removed'}};
        }
        return {
          statusCode: 404,
          body: {status: 'fail', message: 'Article not found'}
        };
      }
    }
  });
}

Terminology

Just to clarify some terminology that will be used throughout these docs:

(HTTP) Method:

Endpoint:

Route:

Writing A Restivus API

Restivus is a server-only package. Attempting to access any of its methods from the client will result in an error.

Configuration Options

The following configuration options are available when initializing an API using new Restivus(options):

apiPath
auth
defaultHeaders
defaultOptionsEndpoint
enableCors
onLoggedIn
onLoggedOut
prettyJson
useDefaultAuth
version

Here's a sample configuration with the complete set of options:

Warning! For demo purposes only - using this configuration is not recommended!

CoffeeScript
  new Restivus
    apiPath: 'my-api/'
    auth:
      token: 'auth.apiKey'
      user: ->
        userId: @request.headers['user-id']
        token: @request.headers['login-token']
    defaultHeaders:
      'Content-Type': 'application/json'
    onLoggedIn: -> console.log "#{@user.username} (#{@userId}) logged in"
    onLoggedOut: -> console.log "#{@user.username} (#{@userId}) logged out"
    prettyJson: true
    useDefaultAuth: true
    version: 'v1'
JavaScript
  new Restivus({
    apiPath: 'my-api/',
    auth: {
      token: 'auth.apiKey',
      user: function () {
        return {
          userId: this.request.headers['user-id'],
          token: this.request.headers['login-token']
        };
      }
    },
    defaultHeaders: {
      'Content-Type': 'application/json'
    },
    onLoggedIn: function () {
      console.log(this.user.username + ' (' + this.userId + ') logged in');
    },
    onLoggedOut: function () {
      console.log(this.user.username + ' (' + this.userId + ') logged out');
    },
    prettyJson: true,
    useDefaultAuth: true,
    version: 'v1'
  });

Defining Collection Routes

One of the most common uses for a REST API is exposing a set of operations on your collections. Well, you're in luck, because this is almost too easy with Restivus! All available REST endpoints (except patch and options, for now) can be generated for a Mongo Collection using Restivus#addCollection(). This generates two routes by default:

/api/<collection>

/api/<collection>/:id

Collection

The first - and only required - parameter of Restivus#addCollection() is a Mongo Collection. Please check out the Meteor docs for more on creating collections. The Meteor.users collection will have [special endpoints] (#users-collection-endpoints) generated.

Collection Options

Route and endpoint configuration options are available in Restivus#addCollection() (as the 2nd, optional parameter).

Route Configuration

The top level properties of the options apply to both routes that will be generated (/api/<collection> and /api/<collection>/:id):

path
routeOptions
excludedEndpoints
endpoints

Endpoint Configuration

By default, each of the endpoints listed above is undefined, which means it will be generated with any default route options. If you need finer control over your endpoints, each can be defined as an object containing the following properties:

authRequired
roleRequired
action

Request and Response Structure

All responses generated by Restivus follow the JSend format, with one minor tweak: failures have an identical structure to errors. Successful responses will have a status code of 200, unless otherwise indicated. Sample requests and responses for each endpoint are included below:

post

Request:

curl -X POST http://localhost:3000/api/articles/ -d "title=Witty Title" -d "author=Jack Rose"

Response:

Status Code: 201

{
  "status": "success",
  "data": {
    "_id": "LrcEYNojn5N7NPRdo",
    "title": "Witty Title",
    "author": "Jack Rose"
  }
}

getAll

Request:

curl -X GET http://localhost:3000/api/articles/

Response:

{
  "status": "success",
  "data": [
    {
      "_id": "LrcEYNojn5N7NPRdo",
      "title": "Witty Title!",
      "author": "Jack Rose",
    },
    {
      "_id": "7F89EFivTnAcPMcY5",
      "title": "Average Stuff",
      "author": "Joe Schmoe",
    }
  ]
}

get

Request:

curl -X GET http://localhost:3000/api/articles/LrcEYNojn5N7NPRdo

Response:

{
  "status": "success",
  "data": {
    "_id": "LrcEYNojn5N7NPRdo",
    "title": "Witty Title",
    "author": "Jack Rose",
  }
}

put

Request:

curl -X PUT http://localhost:3000/api/articles/LrcEYNojn5N7NPRdo -d "title=Wittier Title" -d "author=Jaclyn Rose"

Response:

{
  "status": "success",
  "data": {
    "_id": "LrcEYNojn5N7NPRdo",
    "title": "Wittier Title",
    "author": "Jaclyn Rose"
  }
}

patch

Request:

curl -X PATCH http://localhost:3000/api/articles/LrcEYNojn5N7NPRdo -d "author=J. K. Rowling"

Response:

{
  "status": "success",
  "data": {
    "_id": "LrcEYNojn5N7NPRdo",
    "title": "Wittier Title",
    "author": "J. K. Rowling"
  }
}

delete

Request:

curl -X DELETE http://localhost:3000/api/articles/LrcEYNojn5N7NPRdo

Response:

{
  "status": "success",
  "data": {
    "message": "Item removed"
  }
}

Users Collection Endpoints

A few special exceptions have been made for routes added for the Meteor.users collection. For now, the majority of the operations are limited to read access to the user._id and read/write access to the user.profile. All route and endpoint options are identical to those described for all other collections above. No options have been configured in the examples below; however, it is highly recommended that role permissions be setup (or at the absolute least, authentication required) for the delete endpoint. Below are sample requests and responses for the users collection.

Create collection:

Api.addCollection(Meteor.users);

post

Request: POST http://localhost:3000/api/users

{
  "email": "jack@mail.com",
  "password": "password",
  "profile": {
    "firstName": "Jack",
    "lastName": "Rose"
  }
}

Note: The only fields that will be recognized in the request body when creating a new user are email, username, password, and profile. These map directly to the parameters of the same name in the Accounts.createUser() method, so check that out for more information on how those fields are handled.

Response:

Status Code: 201

{
  "status": "success",
  "data": {
    "_id": "oFpdgAMMr7F5A7P3a",
    "profile": {
      "firstName": "Jack",
      "lastName": "Rose"
    }
  }
}

getAll

Request:

curl -X GET http://localhost:3000/api/users/

Response:

{
  "status": "success",
  "data": [
    {
      "_id": "nBTnv83sTrf38fFTi",
      "profile": {
        "firstName": "Anthony",
        "lastName": "Reid"
      }
    },
    {
      "_id": "oFpdgAMMr7F5A7P3a",
      "profile": {
        "firstName": "Jack",
        "lastName": "Rose"
      }
    }
  ]
}

get

Request:

curl -X GET http://localhost:3000/api/users/oFpdgAMMr7F5A7P3a

Response:

{
  "status": "success",
  "data": {
    "_id": "oFpdgAMMr7F5A7P3a",
    "profile": {
      "firstName": "Jack",
      "lastName": "Rose"
    }
  }
}

put

Request: PUT http://localhost:3000/api/users/oFpdgAMMr7F5A7P3a

{
    "firstName": "Jaclyn",
    "age": 25
}

Note: The data included in the request body will completely overwrite the user.profile field of the User document

Response:

{
  "status": "success",
  "data": {
    "_id": "oFpdgAMMr7F5A7P3a",
    "profile": {
      "firstName": "Jaclyn",
      "age": "25"
    }
  }
}

delete

Request:

curl -X DELETE http://localhost:3000/api/users/oFpdgAMMr7F5A7P3a

Response:

{
  "status": "success",
  "data": {
    "message": "User removed"
  }
}

Defining Custom Routes

Routes are defined using Restivus#addRoute(). A route consists of a path and a set of endpoints defined at that path.

Path Structure

The path is the 1st parameter of Restivus#addRoute. You can pass it a string or regex. If you pass it test/path, the full path will be https://yoursite.com/api/test/path.

Paths can have variable parameters. For example, you can create a route to show a post with a specific id. The id is variable depending on the post you want to see such as "/articles/1" or "/articles/2". To declare a named parameter in the path, use the : syntax followed by the parameter name. When a user goes to that URL, the actual value of the parameter will be stored as a property on this.urlParams in your endpoint function.

In this example we have a parameter named _id. If we navigate to the /post/5 URL in our browser, inside of the GET endpoint function we can get the actual value of the _id from this.urlParams._id. In this case this.urlParams._id => 5.

CoffeeScript:
# Given a URL like "/post/5"
Api.addRoute '/post/:_id',
  get: ->
    id = @urlParams._id # "5"
JavaScript:
// Given a URL "/post/5"
Api.addRoute('/post/:_id', {
  get: function () {
    var id = this.urlParams._id; // "5"
  }
});

You can have multiple URL parameters. In this example, we have an _id parameter and a commentId parameter. If you navigate to the URL /post/5/comments/100 then inside your endpoint function this.urlParams._id => 5 and this.urlParams.commentId => 100.

CoffeeScript:
# Given a URL "/post/5/comments/100"
Api.addRoute '/post/:_id/comments/:commentId',
  get: ->
    id = @urlParams._id # "5"
    commentId = @urlParams.commentId # "100"
JavaScript:
// Given a URL "/post/5/comments/100"
Api.addRoute('/post/:_id/comments/:commentId', {
  get: function () {
    var id = this.urlParams._id; // "5"
    var commentId = this.urlParams.commentId; // "100"
  }
});

If there is a query string in the URL, you can access that using this.queryParams.

Coffeescript:
# Given the URL: "/post/5?q=liked#hash_fragment"
Api.addRoute '/post/:_id',
  get: ->
    id = @urlParams._id
    query = @queryParams # query.q -> "liked"
JavaScript:
// Given the URL: "/post/5?q=liked#hash_fragment"
Api.addRoute('/post/:_id', {
  get: function () {
    var id = this.urlParams._id;
    var query = this.queryParams; // query.q -> "liked"
  }
});

Route Options

The following options are available in Restivus#addRoute (as the 2nd, optional parameter):

authRequired
roleRequired

Defining Endpoints

The last parameter of Restivus#addRoute is an object with properties corresponding to the supported HTTP methods. At least one method must have an endpoint defined on it. The following endpoints can be defined in Restivus:

These endpoints can be defined one of two ways. First, you can simply provide a function for each method you want to support at the given path. The corresponding endpoint will be executed when that type of request is made at that path.

For finer-grained control over each endpoint, you can also define each one as an object containing the endpoint action and some addtional configuration options.

Endpoint Configuration

An action is required when configuring an endpoint. All other configuration settings are optional, and will get their default values from the route.

action
authRequired
roleRequired
CoffeeScript
Api.addRoute 'articles', {authRequired: true},
  get:
    authRequired: false
    action: ->
      # GET api/articles
  post: ->
    # POST api/articles
  put: ->
    # PUT api/articles
  patch: ->
    # PATCH api/articles
  delete: ->
    # DELETE api/articles
  options: ->
    # OPTIONS api/articles
JavaScript
Api.addRoute('articles', {authRequired: true}, {
  get: {
    authRequired: false,
    action: function () {
      // GET api/articles
    }
  },
  post: function () {
    // POST api/articles
  },
  put: function () {
    // PUT api/articles
  },
  patch: function () {
    // PATCH api/articles
  },
  delete: function () {
    // DELETE api/articles
  },
  options: function () {
    // OPTIONS api/articles
  }
});

In the above examples, all the endpoints except the GETs will require [authentication] (#authenticating).

Endpoint Context

Each endpoint has access to:

this.user
this.userId
this.urlParams
this.queryParams
this.bodyParams
this.request
this.response
this.done()
this.<endpointOption>

All endpoint configuration options can be accessed by name (e.g., this.roleRequired). Within an endpoint, all options have been completely resolved, meaning all configuration options set on an endpoint's route will already be applied to the endpoint as defaults. So if you set authRequired: true on a route and do not set the authRequired option on one if its endpoints, this.authRequired will still be true within that endpoint, since the default will already have been applied from the route.

Response Data

You can return a raw string:

return "That's current!";

A JSON object:

return { json: 'object' };

A raw array:

return [ 'red', 'green', 'blue' ];

Or include a statusCode or headers. At least one must be provided along with the body:

return {
  statusCode: 404,
  headers: {
    'Content-Type': 'text/plain',
    'X-Custom-Header': 'custom value'
  },
  body: 'There is nothing here!'
};

All responses contain the following defaults, which will be overridden with any provided values:

statusCode
headers

Versioning an API

We can't always get an API right on the first try (in fact, most people don't). Eventually, we find ourselves needing to maintain different versions of our API. This allows clients to convert at their own convenience, while providing the latest and greatest API to those ready to consume it.

Currently, there is only a single versioning strategy supported in Restivus: URL path versioning. In this strategy, the version of the API is appended to the base path of all routes belonging to that API. This allows us to easily maintain multiple versions of an API, each with their own set of configuration options. Here's a [good write-up] (http://www.troyhunt.com/2014/02/your-api-versioning-is-wrong-which-is.html) on some of the different API versioning strategies.

CoffeeScript
# Configure first version of the API
ApiV1 = new Restivus
  version: 'v1'
  useDefaultAuth: true
  prettyJson: true

# Maps to api/v1/items and api/v1/items/:id
ApiV1.addCollection Items
  routeOptions: authRequired: true

# Maps to api/v1/custom
ApiV1.addRoute 'custom',
  get: ->
    'get something'

# Configure another version of the API (with a different set of config options if needed)
ApiV2 = new Restivus
  version: 'v2'
  enableCors: false

# Maps to api/v2/items and api/v2/items/:id (with auth requirement removed in this version)
ApiV2.addCollection Items

# Maps to api/v2/custom (notice the different return value)
ApiV2.addRoute 'custom',
  get: ->
    status: 'success'
    data: 'get something different'
JavaScript
// Configure first version of the API
var ApiV1 = new Restivus({
  version: 'v1',
  useDefaultAuth: true,
  prettyJson: true
});

// Maps to api/v1/items and api/v1/items/:id
ApiV1.addCollection(Items, {
  routeOptions: { authRequired: true }
});

// Maps to api/v1/custom
ApiV1.addRoute('custom', {
  get: function () {
    return 'get something';
  }
});

// Configure another version of the API (with a different set of config options if needed)
var ApiV2 = new Restivus({
  version: 'v2',
  enableCors: false
});

// Maps to api/v2/items and api/v2/items/:id (with auth requirement removed in this version)
ApiV2.addCollection(Items);

// Maps to api/v2/custom (notice the different return value)
ApiV2.addRoute('custom', {
  get: function () {
    return {
      status: 'success',
      data: 'get something different'
    };
  }
});

Documenting an API

What's a REST API without awesome docs? I'll tell you: absolutely freaking useless. So to fix that, we use and recommend apiDoc. It allows you to generate beautiful and extremely handy API docs from your JavaScript or CoffeeScript comments. It supports other comment styles as well, but we're Meteorites, so who cares? Check it out. Use it.

Consuming A Restivus API

The following uses the above code.

Basic Usage

We can call our POST /articles/:id/comments endpoint the following way. Note the /api/ in the URL (defined with the api_path option above):

curl -d "message=Some message details" http://localhost:3000/api/articles/3/comments

Note: There is a 50mb limit on requests. If you need this limit increased, please file a GitHub Issue.

Authenticating

Warning: Make sure you're using HTTPS, otherwise this is insecure!

Default Authentication

Note: To use the default authentication, you must first create a user with the accounts-password package. You can do this with Restivus if you setup a POST collection endpoint for the Meteor.users collection.

Logging In

If you have useDefaultAuth set to true, you now have a POST /api/login endpoint that returns a userId and authToken. You must save these, and include them in subsequent requests. In addition to the password, the login endpoint requires one of the following parameters (via the request body):

A login will look something like

curl http://localhost:3000/api/login/ -d "username=test&password=password"

The password can be SHA-256 hashed on the client side, in which case your request would look like

curl http://localhost:3000/api/login/ -d "username=test&password=sha-256-password&hashed=true"

And the response will look like

{ status: "success", data: {authToken: "f2KpRW7KeN9aPmjSZ", userId: fbdpsNf4oHiX79vMJ} }

You'll need to save the userId and token on the client, for subsequent authenticated requests.

Logging Out

You also have an authenticated POST /api/logout endpoint for logging a user out. If successful, the auth token that is passed in the request header will be invalidated (removed from the user account), so it will not work in any subsequent requests.

curl http://localhost:3000/api/logout -X POST -H "X-Auth-Token: f2KpRW7KeN9aPmjSZ" -H "X-User-Id: fbdpsNf4oHiX79vMJ"

Authenticated Calls

For any endpoints that require the default authentication, you must include the userId and authToken with each request under the following headers:

curl -H "X-Auth-Token: f2KpRW7KeN9aPmjSZ" -H "X-User-Id: fbdpsNf4oHiX79vMJ" http://localhost:3000/api/articles/

Upgrading Restivus

To update Restivus to the latest version:

> meteor update nimble:restivus

Or to update Restivus to a specific version:

> meteor add nimble:restivus@=<version_number>

For example, to update restivus to v0.7.0:

> meteor add nimble:restivus@=0.7.0

Please check the change log before updating, for more information about the changes between each version. More detailed instructions for updating between major versions are included below.

Upgrading to 0.8.0

The most noticeable difference in v0.8.0 is that Restivus is now exported as a "class" instead of an object. API configuration has also been moved from Restivus.configure() (which has been removed), to the Restivus constructor. This means that instead of being forced to configure a single Restivus API with

Restivus.configure({
  apiPath: 'only-api',
  ...
});

Restivus.addRoute('example');

you can configure as many separate APIs as you need with

FirstApi = new Restivus({
  apiPath: 'first-api',
  ...
});

SecondApi = new Restivus({
  apiPath: 'second-api',
  ...
});

// Maps to /first-api/example
FirstApi.addRoute('example', ...);

// Maps to /second-api/example
SecondApi.addRoute('example', ...);

This update makes it possible to maintain multiple versions of an API.

One other significant (but not API-breaking) change is that iron:router has been replaced by simple:json-routes as the server-side router for Restivus. This means that Restivus should no longer [interfere with other routers] (https://github.com/kahmali/meteor-restivus/issues/24) (client or server), or do other [annoying] (https://github.com/kahmali/meteor-restivus/issues/35) [things] (https://github.com/kahmali/meteor-restivus/issues/43). Special thanks to [Sashko Stubailo] (https://github.com/stubailo) for his work on simple:json-routes, and for handling the conversion from iron:router to simple:json-routes in Restivus!

Some other notable changes are:

For a complete list of changes, check out the [change log] (https://github.com/kahmali/meteor-restivus/blob/devel/CHANGELOG.md#v080---2015-07-06).

Upgrading to 0.7.0

WARNING! All clients consuming a Restivus API with the default authentication will need to reauthenticate after this update

Restivus used to store the account login token in the Meteor.user document at services.resume.loginTokens.token. Now, to match Meteor's current implementation, the account login token is stored as a hashed token at services.resume.loginTokens.hashedToken. This means that all clients using the default authentication in a Restivus API will need to reauthenticate with their username/email and password after this update, as their existing tokens will be rendered invalid.

Upgrading to 0.6.1

Restivus v0.6.1 brings support for easily generating REST endpoints for your Mongo Collections, and with that comes a few API-breaking changes:

Resources

Plugins

Restivus Swagger

Change Log

A detailed list of the changes between versions can be found in the [change log] (https://github.com/kahmali/meteor-restivus/blob/master/CHANGELOG.md).

Contributing

Contributions to Restivus are welcome and appreciated! If you're interested in contributing, please check out the guidelines before getting started.

Thanks

Thanks to the developers over at Differential for RestStop2, where we got our inspiration for this package and stole tons of ideas and code, as well as the Sashko Stubailo from MDG, for his work on simple:json-routes, including the Restivus conversion from Iron Router.

Also, thanks to the following projects, which RestStop2 was inspired by:

License

MIT License. See LICENSE for details.