Skip to content

Tutorial Create a basic API with public gateway

Sebastien Filion edited this page May 21, 2018 · 3 revisions

Create a basic API with public Gateway

Once you learned how to create your first engine, you probably wondered: "Okay, but what's next?". Nucleus implements a communication layer that allows for a decentralized and distributed architecture.
The engine is tasked with interacting with the communication layer while APIs can be used to wrap a third party client.

Basic interaction with the outside world

Taking the image above

  1. A request comes in through the API Gateway engine that manages a HTTP server;
  2. The API Gateway engine processes every requests and normalize them for the Nucleus communication layer;
  3. The requests are queued as Action;
  4. Engines routinely execute pending actions using local action configuration;
  5. Once an action is correctly executed, it is marked as "Completed" allowing the origin engine to relay the answer;
  6. If an action fails, it will similarly mark it as "Failed" and pass it to the origin engine to relay the error;

Let's build such a system!

To convey that the engines can be distributed on different, thread, environment, computer, city or even country, we will separate them into different directories: the Core's and the API Gateway's.

File structure

The Core's engine will be very similar to the one we created before, but two options will be set: automaticallyAutodiscover and automaticallyRetrievePendingActions.
This will tell the engine to use autodiscovery and to automatically retrieve pending actions.

"use strict";
// Core/Core.engine.js
const { NucleusEngine } = require('idex.nucleus');

class CoreEngine extends NucleusEngine {

  constructor () {

    super('Core', {
      automaticallyAutodiscover: true,
      automaticallyRetrievePendingActions: true
    });
  }

}

module.exports = CoreEngine;

For the User's API, we will create a very simple persistent storage API.
An API is nothing more than a bunch of overly documented functions... Every API functions must return a promise. If you must handle errors, make sure to still reject the promise.
Note that an API function can't have access to the communication layer but will be provided a datastore connection.

"use strict";

// Core/User.api.js
module.exports = {
  /**
   * Creates an user.
   * 
   * @Nucleus ActionName CreateUser
   * 
   * @argument {Object} userAttributes
   * 
   * @returns {Promise<{ user: User }>}
   */
  createUser (userAttributes) {
    const { $datastore } = this;
    
    return $datastore.createItem(`User:${userAttributes.ID}`, userAttributes)
      .return({ user: userAttributes });
  },
  
  /**
   * Removes an user given its ID.
   * 
   * @Nucleus ActionName RemoveUserByID
   * 
   * @argument {String} userID
   * 
   * @returns {Promise<{ userID: String }>}
   */
  removeUserByID (userID) {
    const { $datastore } = this;

    return $datastore.removeItemByName(`User:${userID}`)
      .return({ userID });
  },
  
  /**
   * Retrieves an user given its ID.
   * 
   * @Nucleus ActionName RetrieveUserByID
   * 
   * @argument {String} userID
   * 
   * @returns {Promise<{ user: User }>}
   */
  retrieveUserByID (userID) {
    const { $datastore } = this;

    return $datastore.retrieveItemByName(`User:${userID}`)
      .then(user => { user });
  },
  
  /**
   * Updates an user given its ID.
   * 
   * @Nucleus ActionName UpdateUserByID
   * 
   * @argument {String} userID
   * @argument {String} userAttributes
   * 
   * @returns {Promise<{ user: User }>}
   */
  updateUserByID (userID, userAttributes) {
    const { $datastore } = this;

    return $datastore.createItem(`User:${userID}`, userAttributes)
      .return({ user: userAttributes });
  }
};

On the other side, we can implement a quick HTTP server using express.

"use strict";

const bodyParser = require('body-parser');
const express = require('express');
const http = require('http');
const uuid = require('uuid');

const { NucleusEngine } = require('idex.nucleus');

const HTTP_PORT = 3000;

class APIGatewayEngine extends NucleusEngine {

  constructor () {
    super('APIGateway');

    this.$$application = express();
    this.$$httpServer = http.createServer(this.$$application);

    // Once the engine is initialized...
    this.$$promise = this.$$promise
      .then(() => {

        return new Promise((resolve) => {
          // HTTP server listens on the given port;
          this.$$httpServer.listen(HTTP_PORT, resolve);
        });
      })
      .timeout(1000)
      .then(() => {
        this.$logger.info(`HTTP server is listening on port ${HTTP_PORT}.`);
      })
      .then(this.registerRESTEndpoints.bind(this));
  }

  registerRESTEndpoint () {
    const routeList = [
      [ 'POST', '/user', 'CreateUser' ],
      [ 'DELETE', '/user/:userID', 'RemoveUserByID' ],
      [ 'GET', '/user/:userID', 'RetrieveUserByID' ],
      [ 'PATCH', '/user/:userID', 'UpdateUserById' ],
    ];
    
    this.$$application.use(bodyParser.json());
    this.$$application.use(bodyParser.urlencoded({ extended: true }));
    
    this.$$application.use((request, response, next) => {
  
      response.header("Access-Control-Allow-Origin", request.headers.origin);
      response.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
      response.header("Access-Control-Allow-Headers", "X-Requested-With, X-HTTP-Method-Override, Content-Type, Authorization, Accept");
      response.header("Access-Control-Allow-Credentials", "true");
      response.header("X-Powered-By", "Nucleus");
  
  
      if (request.method === 'OPTIONS') {
        response.header("Access-Control-Max-Age", 1000 * 60 * 10);
        return response.status(204).end();
      }
      next();
    });

    routeList
      .forEach(([ endpointVerb, endpointPath, actionName ]) => {
        this.$$application[endpointVerb.toLowerCase()](endpointPath, async (request, response) => {
          try {
            const actionMessage = Object.assign({}, request.body, request.params, request.query);
            const actionResponse = await this.publishActionByNameAndHandleResponse(actionName, actionMessage, uuid.v4());

            response.status(200).send(actionResponse).end();
          } catch (error) {

            response.status(500).send(error).end();
          }
        });
      });
  }

}

module.exports = APIGatewayEngine;
Clone this wiki locally