maka:rest

v4.0.9Published 2 weeks ago

Maka-Rest

REST APIs for the Best of Us!

Table of Contents

Getting Started

Installation

You can install Maka-Rest using Meteor's package manager:

> meteor add maka:rest

Or using maka-cli

> maka add maka:rest

Quick Start

1	import MakaRest from 'meteor/maka:rest';

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 Maka-Rest:

1Items = new Mongo.Collection('items');
2Articles = new Mongo.Collection('articles');
3
4if (Meteor.isServer) {
5  // Global API configuration
6  const Api = new MakaRest({
7    useDefaultAuth: true,
8    prettyJson: true
9  });
10
11  // Maps to: /api/articles/:id
12  Api.addRoute('articles/:id', {authRequired: true}, {
13    get() {
14      return Articles.findOne(this.urlParams.id);
15    },
16    delete: {
17      roleRequired: ['author', 'admin'],
18      action() {
19        if (Articles.remove(this.urlParams.id)) {
20          return {status: 'success', data: {message: 'Article removed'}};
21        }
22        return {
23          statusCode: 404,
24          body: {status: 'fail', message: 'Article not found'}
25        };
26      }
27    }
28  });
29}

Terminology

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

(HTTP) Method:

  • The type of HTTP request (e.g., GET, PUT, POST, etc.)

Endpoint:

  • The function executed when a request is made at a given URL path for a specific HTTP method

Route:

  • A URL path and its set of configurable endpoints

Writing A Maka-Rest API

Maka-Rest 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 Maka-Rest(options):

apiPath
  • String
  • Default: 'api/'
  • The base path for your API. If you use 'api' and add a route called 'users', the URL will be https://yoursite.com/api/users/.
auth
  • Object
    • token String
      • Default: 'services.resume.loginTokens.hashedToken'
      • The path to the hashed auth token in the Meteor.user document. This location will be checked for a matching token if one is returned in auth.user().
    • user Function
      • Default: Get user from X-Auth-Token headers

        1function() {
        2  return {
        3    token: Accounts._hashLoginToken(this.request.headers['x-auth-token'])
        4  };
        5}
      • Provides one of two levels of authentication, depending on the data returned. The context within this function is the endpoint context without this.user and completes successfully, the authenticated user and their ID will be attached to the endpoint context.

        For either level of auth described above, you can optionally return a custom error response by providing that response in an error field of your response object. The error value can be any valid response. If an error field exists in the object returned from your custom auth function, all other fields will be ignored. Do not provide an error value if you intend for the authentication to pass successfully.

defaultHeaders
defaultOptionsEndpoint
  • Endpoint
  • Default: undefined
  • If an endpoint is provided, it will be used as the OPTIONS endpoint on all routes, except those that have one manually defined. This can be used to DRY up your API, since OPTIONS endpoints will frequently respond generically across all routes.
enableCors
  • Boolean
  • Default: true
  • If true, enables cross-origin resource sharing ([CORS]). This allows your API to receive requests from any domain (when false, the API will only accept requests from the domain where the API is being hosted. Note: Only applies to requests originating from browsers).
prettyJson
  • Boolean
  • Default: false
  • If true, render formatted JSON in response.
useDefaultAuth
  • Boolean
  • Default: false
  • If true, POST /login and GET /logout endpoints are added to the API. See [Authenticating] (#authenticating) for details on using these endpoints.
version
  • String
  • Default: null
  • URL path versioning is the only type of API versioning currently available, so if a version is provided, it's appended to the base path of all routes that belong to that API
    1// Base URL path: my-api/v1/
    2ApiV1 = new MakaRest({
    3  apiPath: 'my-api/',
    4  version: 'v1'
    5});
    6
    7// Base URL path: my-api/v2/
    8ApiV2 = new MakaRest({
    9  apiPath: 'my-api/',
    10  version: 'v2'
    11});

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

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

1  new MakaRest({
2    apiPath: 'my-api/',
3    auth: {
4      token: 'auth.apiKey',
5      user: function () {
6        return {
7          userId: this.request.headers['user-id'],
8          token: this.request.headers['login-token']
9        };
10      }
11    },
12    defaultHeaders: {
13      'Content-Type': 'application/json'
14    },
15    prettyJson: true,
16    useDefaultAuth: true,
17    version: 'v1'
18  });

Route Configuration

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

path
  • String
  • The base path for the generated routes. Given a path 'other-path', routes will be generated at 'api/other-path' and 'api/other-path/:id'
routeOptions
  • Object
  • authRequired Boolean
    • Default: false
    • If true, all endpoints on these routes will return a 401 if the user is not properly authenticated.
  • roleRequired String or Array of Strings
    • Default: undefined (no role required)
    • The acceptable user roles for all endpoints on this route (e.g., 'admin', ['admin', 'dev']). Additional role permissions can be defined on specific endpoints. If the authenticated user does not belong to at least one of the accepted roles, a 403 is returned. Since a role cannot be verified without an authenticated user, setting the roleRequired implies authRequired: true, so that option can be omitted without any consequence. For more on setting up roles, check out the [alanning:roles][alanning-roles] package.
excludedEndpoints
  • String or Array of Strings
  • Default: undefined
  • The names of the endpoints that should not be generated (see the endpoints option below for a complete list of endpoint names).
endpoints
  • Object
  • Default: undefined (all available endpoints generated)
  • Each property of this object corresponds to a REST endpoint. In addition to the excludedEndpoints list, you can also prevent an endpoint from being generated by setting its value to false. All other endpoints will be generated. The complete set of configurable properties on these endpoints is described in the Endpoint Configuration section below. Here is a list of all available endpoints, including their corresponding HTTP method, path, and a short description of their behavior:
    • getAll Endpoint
      • GET /api/collection
      • Return a list of all entities within the collection (filtered searching via query params coming soon!).
    • post Endpoint
      • POST /api/collection
      • Add a new entity to the collection. All data passed in the request body will be copied into the newly created entity. Warning: This is unsafe for now, as no type or bounds checking is done.
    • get Endpoint
      • GET /api/collection/:id
      • Return the entity with the given :id.
    • put Endpoint
      • PUT /api/collection/:id
      • Completely replace the entity with the given :id with the data contained in the request body. Any fields not included will be removed from the document in the collection.
    • patch Endpoint
      • PATCH /api/collection/:id
      • Partially modify the entity with the given :id with the data contained in the request body. Only fields included will be modified.
    • delete Endpoint
      • DELETE /api/collection/:id
      • Remove the entity with the given :id from the collection.

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
  • Boolean
  • Default: undefined
  • If true, this endpoint will return a 401 if the user is not properly [authenticated] (#authenticating). If defined, this overrides the option of the same name defined on the entire route.
roleRequired
  • String or Array of Strings
  • Default: undefined (no role required)
  • The acceptable user roles for this endpoint (e.g., 'admin', ['admin', 'dev']). These roles will be accepted in addition to any defined over the entire route. If the authenticated user does not belong to at least one of the accepted roles, a 403 is returned. Since a role cannot be verified without an authenticated user, setting the roleRequired implies authRequired: true, so that option can be omitted without any consequence. For more on setting up roles, check out the [alanning:roles][alanning-roles] package.
action
  • Function
  • Default: undefined (Default endpoint generated)
  • If you need to completely override the default endpoint behavior, you can provide a function that will be executed when the corresponding request is made. No parameters are passed; instead, this contains the endpoint context, with properties including the URL and query parameters.

Request and Response Structure

All responses generated by Maka-Rest 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

1{
2  "status": "success",
3  "data": {
4    "_id": "LrcEYNojn5N7NPRdo",
5    "title": "Witty Title",
6    "author": "Jack Rose"
7  }
8}

getAll

Request:

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

Response:

1{
2  "status": "success",
3  "data": [
4    {
5      "_id": "LrcEYNojn5N7NPRdo",
6      "title": "Witty Title!",
7      "author": "Jack Rose",
8    },
9    {
10      "_id": "7F89EFivTnAcPMcY5",
11      "title": "Average Stuff",
12      "author": "Joe Schmoe",
13    }
14  ]
15}

get

Request:

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

Response:

1{
2  "status": "success",
3  "data": {
4    "_id": "LrcEYNojn5N7NPRdo",
5    "title": "Witty Title",
6    "author": "Jack Rose",
7  }
8}

put

Request:

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

Response:

1{
2  "status": "success",
3  "data": {
4    "_id": "LrcEYNojn5N7NPRdo",
5    "title": "Wittier Title",
6    "author": "Jaclyn Rose"
7  }
8}

patch

Request:

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

Response:

1{
2  "status": "success",
3  "data": {
4    "_id": "LrcEYNojn5N7NPRdo",
5    "title": "Wittier Title",
6    "author": "J. K. Rowling"
7  }
8}

delete

Request:

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

Response:

1{
2  "status": "success",
3  "data": {
4    "message": "Item removed"
5  }
6}

Defining Custom Routes

Routes are defined using Maka-Rest#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 Maka-Rest#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.

1// Given a URL "/post/5"
2Api.addRoute('/post/:_id', {
3  get: function () {
4    const id = this.urlParams._id; // "5"
5  }
6});

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.

1// Given a URL "/post/5/comments/100"
2Api.addRoute('/post/:_id/comments/:commentId', {
3  get: function () {
4    const id = this.urlParams._id; // "5"
5    const commentId = this.urlParams.commentId; // "100"
6  }
7});

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

1// Given the URL: "/post/5?q=liked#hash_fragment"
2Api.addRoute('/post/:_id', {
3  get: function () {
4    const id = this.urlParams._id;
5    const query = this.queryParams; // query.q -> "liked"
6  }
7});

Route Options

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

authRequired
  • Boolean
  • Default: false
  • If true, all endpoints on this route will return a 401 if the user is not properly authenticated.
roleRequired
  • String or Array of Strings
  • Default: undefined (no role required)
  • A string or array of strings corresponding to the acceptable user roles for all endpoints on this route (e.g., 'admin', ['admin', 'dev']). Additional role permissions can be defined on specific endpoints. If the authenticated user does not belong to at least one of the accepted roles, a 403 is returned. Since a role cannot be verified without an authenticated user, setting the roleRequired implies authRequired: true, so that option can be omitted without any consequence. For more on setting up roles, check out the [alanning:roles][alanning-roles] package.

Defining Endpoints

The last parameter of Maka-Rest#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 Maka-Rest:

  • get
  • post
  • put
  • patch
  • delete
  • options

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
  • Function
  • Default: undefined
  • A function that will be executed when a request is made for the corresponding HTTP method.
authRequired
  • String
  • Default: Route.authRequired
  • If true, this endpoint will return a 401 if the user is not properly authenticated. Overrides the option of the same name defined on the entire route.
roleRequired
  • String or Array of Strings
  • Default: Route.roleRequired
  • The acceptable user roles for this endpoint (e.g., 'admin', ['admin', 'dev']). These roles will be accepted in addition to any defined over the entire route. If the authenticated user does not belong to at least one of the accepted roles, a 403 is returned. Since a role cannot be verified without an authenticated user, setting the roleRequired implies authRequired: true, so that option can be omitted without any consequence. For more on setting up roles, check out the [alanning:roles][alanning-roles] package.
1Api.addRoute('articles', {authRequired: true}, {
2  get: {
3    authRequired: false,
4    action: function () {
5      // GET api/articles
6    }
7  },
8  post: function () {
9    // POST api/articles
10  },
11  put: function () {
12    // PUT api/articles
13  },
14  patch: function () {
15    // PATCH api/articles
16  },
17  delete: function () {
18    // DELETE api/articles
19  },
20  options: function () {
21    // OPTIONS api/articles
22  }
23});

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

Endpoint Context

Each endpoint has access to:

this.user
  • Meteor.user
  • The authenticated Meteor.user. Only available if authRequired is true and a user is successfully authenticated. If not, it will be undefined.
this.userId
  • String
  • The authenticated user's Meteor.userId. Only available if authRequired is true and a user is successfully authenticated. If not, it will be undefined.
this.urlParams
  • Object
  • Non-optional parameters extracted from the URL. A parameter id on the path articles/:id would be available as this.urlParams.id.
this.queryParams
  • Object
  • Optional query parameters from the URL. Given the URL https://yoursite.com/articles?likes=true, this.queryParams.likes => true.
this.bodyParams
  • Object
  • Parameters passed in the request body. Given the request body { "friend": { "name": "Jack" } }, this.bodyParams.friend.name => "Jack".
this.request
  • [Node request object][node-request]
this.response
  • [Node response object][node-response]
  • If you handle the response yourself using this.response.write() or this.response.writeHead() you must call this.done(). In addition to preventing the default response (which will throw an error if you've initiated the response yourself), it will also close the connection using this.response.end(), so you can safely omit that from your endpoint.
this.done()
  • Function

  • Must be called after handling the response manually with this.response.write() or this.response.writeHead(). This must be called immediately before returning from an endpoint.

    1Api.addRoute('manualResponse', {
    2  get: function () {
    3    console.log('Testing manual response');
    4    this.response.write('This is a manual response');
    5    this.done();  // Must call this immediately before return!
    6  }
    7});
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:

1return "That's current!";

A JSON object:

1return { json: 'object' };

A raw array:

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

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

1return {
2  statusCode: 404,
3  headers: {
4    'Content-Type': 'text/plain',
5    'X-Custom-Header': 'custom value'
6  },
7  body: 'There is nothing here!'
8};

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

statusCode
  • Default: 200
headers
  • Default:
    • Content-Type: application/json
    • Access-Control-Allow-Origin: *
      • This is a [CORS-compliant header][cors] that allows requests to be made to the API from any domain. Without this, requests from within the browser would only be allowed from the same domain the API is hosted on, which is typically not the intended behavior. This can be disabled by default, or also by returning a header of the same name with a domain specified (usually the domain the API is being hosted on).

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 Maka-Rest: 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.

1// Configure first version of the API
2var ApiV1 = new Restivus({
3  version: 'v1',
4  useDefaultAuth: true,
5  prettyJson: true
6});
7
8// Maps to api/v1/items and api/v1/items/:id
9ApiV1.addCollection(Items, {
10  routeOptions: { authRequired: true }
11});
12
13// Maps to api/v1/custom
14ApiV1.addRoute('custom', {
15  get: function () {
16    return 'get something';
17  }
18});
19
20// Configure another version of the API (with a different set of config options if needed)
21var ApiV2 = new Restivus({
22  version: 'v2',
23  enableCors: false
24});
25
26// Maps to api/v2/items and api/v2/items/:id (with auth requirement removed in this version)
27ApiV2.addCollection(Items);
28
29// Maps to api/v2/custom (notice the different return value)
30ApiV2.addRoute('custom', {
31  get: function () {
32    return {
33      status: 'success',
34      data: 'get something different'
35    };
36  }
37});

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 Maka-Rest 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 Maka-Rest 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):

  • email: An email address associated with your Meteor.user account
  • username: The username associated with your Meteor.user account
  • user: Note: This is for legacy purposes only. It is recommended to use one of the options above. Accepts either of the options listed above. Maka-Rest will (very naively) attempt to determine if the value provided is an email, otherwise it will assume it to be the username. This can sometimes lead to unexpected behavior.

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

1{"status":"success","data":{"authToken":"8zXkiThVtm3u7pE-7xacuAIrKF1VTA-WA3LRMogqiRp","when":"2020-08-03T16:21:02.361Z"}}

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: 8zXkiThVtm3u7pE-7xacuAIrKF1VTA-WA3LRMogqiRp"

Authenticated Calls

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

  • X-Auth-Token
curl -H "X-Auth-Token: f2KpRW7KeN9aPmjSZ" http://localhost:3000/api/articles/