Building microservices with Express, GraphQL and Consul

In this post I will attempt to cover quite a lot of things. Here is an outline:

  • Use Consul for service discovery
  • Register a service in Consul
  • Implement a basic health check for Consul
  • Implenent a service discovery mechanism
  • Implement a simple microservice to be “discovered” by the API gateway
  • Implement a basic API gateway with GraphQL

Use Consul for service discovery

Consul is at it’s core a service registry, where services can register themselves, and let other services “discover” them. Consul has many more features, but in this tutorial we are only focusing on service discovery. Download and install Consul, then issue the following command to start Consul, and enable the Web UI.

$ consul agent -dev -ui

You can now browse to: http://localhost:8500/ui/

Register a service in Consul

To register your service in Consul, you can use the HTTP API provided by Consul, or if you are lazy like me, you can just grab a library from npm that does those things for you! Just npm install consul --save and you are good to go.

const consul = require('consul')();

In order to register your service in consul, just do the following:

const service = {
  name: 'my-awesome-service',
  address: IP_ADDRESS
  port: PORT
};
consul.agent.service.register(service, (err) => {
  if (err) {
    // something went wrong
  } else {
    // Great success! Service is now registered in Consul
  }  
});

Have a look in the Consul Web UI, to see your registered service.

Implement a basic health check for Consul

When building services with Express, you can easily implement a simple health check

app.get('/ping', (req, res) => res.status(200).end());

When that is done, just update your service definition when registering it with Consul to include a simple check so Consul knows whether your service is up n runnig or not.

const service = {
  name: 'product-service',
  address: IP_ADDRESS
  port: PORT,
  check: {
    http: `http://${IP_ADDRESS}:${PORT}/ping`,
    interval: '15s',
    notes: 'Basic /ping request'
  }
};

Consul will now perform a GET on the http url above every 15s to determine if the service is up and running. Pretty sweet yes?

Implenent a service discovery mechanism

Now we are going to create a simple helper function to discover services for us, so we can contact them. First we create a lookup function to find the address and port in Consul. Then we use that lookup function when looking up the port and address for a specific service. The serviceRequest function does exactly that.

// serviceRequest.js
const
  request = require('request'),
  consul  = require('consul')();

const lookup = (name) => {
  return new Promise((resolve, reject) => {
    consul.agent.service.list((err, services) => {
      if (err) {
        reject(err);
      } else {
        let service = services[name];
        if (service) {
          resolve(service);
        } else {
          reject('Service not found: ' + name);
        }
      }
    });
  });
};

const createServiceUrl = path => service => `http://${service.Address}:${service.Port}` + path;

const serviceRequest = (serviceName, path) => lookup(serviceName)
  .then(createServiceUrl(path))
  .then(url => new Promise((resolve, reject) => {
    const requestHandler = (error, response, body) => {
      if (error) {
        reject(error)
      } else {
        resolve(JSON.parse(body));
      }
    };
    request({url: url, method: 'GET'}, requestHandler);
  }))
  .catch(error => new Promise((resolve, reject) => {
    reject(error);
  }));

module.exports = serviceRequest;

Implement a simple microservice to be “discovered” by the API gateway

Let’s create a simple product service that the API gateway can discover. It will serve product data from a static json file.

const
  express     = require('express'),
  app         = express(),
  consul      = require('consul')(),
  PORT        = process.env.PORT || 4001,
  IP          = require('../common/ip'),
  productData = require('./products.json');

const SERVICE = {
  name: 'product-service',
  tags: ['product-service'],
  address: IP,
  port: PORT,
  check: {
    http: `http://${IP}:${PORT}/ping`,
    interval: '15s',
    notes: 'Basic /ping request'
  }
};

// Health check for Consul
app.get('/ping', (req, res) => res.status(200).end());

app.get('/products', (req, res) => res.json(productData));

const registerServiceInConsul = () => {
  consul.agent.service.register(SERVICE, (err) => {
    if (err) throw err;
    console.log(`Register ${SERVICE.name} in Consul -- SUCCESSFUL`);
  });
};

const serviceStarted = () => {
  console.log(`Started ${SERVICE.name} @ http://${SERVICE.address}:${SERVICE.port}/`);
  registerServiceInConsul();
};

app.listen(SERVICE.port, serviceStarted);

Here is what the products.json might look like:

[
  {
    "id": 1,
    "name": "Elixir",
    "description": "Elixir is the shit"
  },
  {
    "id": 2,
    "name": "NodeJS",
    "description": "Node is awesome!"
  }
]

Implement a basic API gateway with GraphQL

Install GraphQL and Express middleware like so: npm install graphql express-graphql --save.

First, lets define a simple API that will encapsulate the product service:

// api.js
const serviceRequest = require('./serviceRequest');
const PRODUCT_SERVICE = 'product-service';
exports.products = () => serviceRequest(PRODUCT_SERVICE, '/products');

Now, lets create a GraphQL schema for our products, and use our products API helper function to fetch data from our product service.

// api_gateway.js
const API = require('./api');

const ProductType = new GraphQLObjectType({
  name: 'Product',
  description: 'Product',
  fields: {
    name: {type: GraphQLString},
    description: {type: GraphQLString}
  }
});

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    description: 'Query',
    fields: {
      products: {
        name: 'Product query',
        description: 'Product query description',
        type: new GraphQLList(ProductType),
        resolve: () => API.products() // fetch data from product service
      }
    }
  })
});

app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true
}));