Learn how YOU can build a Serverless GraphQL API on top of a Microservice architecture, part I

softchris

Chris Noring

Posted on April 13, 2019

Learn how YOU can build a Serverless GraphQL API on top of a Microservice architecture, part I

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

The idea with this article is to show how we can build microservices, dockerize them and combine them in a GraphQL API and query it from a Serverless function, how's that for a lot of buzzwords in one? ;) Microservices, Docker, GraphQL, Serverless

This is part of series:

So, it's quite ambitious to create Microservices, Serverless and deploy to the Cloud in one article so this is a two-parter. This is part one. This part deals with Microservices and GraphQL. In part two we make it serverless and deploy it.

In this article we will cover:

  • GraphQL and Microservices, these are two great paradigms that really go quite well together, let's explain how
  • Building Microservices and Dockerizing them, let's create two micros services, each with their own areas of responsibility and let's Dockerize them
  • Implementing GraphQl, we will show how we can define a GraphQL backend with schema and resolver and how to query it, of course, we will also tie in our Micro services here

Resources

We will throw you in head first using Docker, GraphQL and some Serverless with Azure functions. This article is more of a recipe of what you can do with the above techniques so if you feel you need a primer on the above here is a list of posts I've written:

 GraphQL and Microservices

A library and paradigm like GraphQL, is the most useful when it is able to combine different data sources into one and serve that up as one unified API. A developer of the Front end app can then query the data they need by using just one request.

Today it becomes more common to break down a monolithic architecture into microservices, thereby you get many small APIs that works independently. GraphQL and microservices are two paradigms that go really well together. How you wonder? GraphQL is really good at describing schemas but also stitch together different APIs and the end result is something that's really useful for someone building an app as querying for data will be very simple.

Different APIs is exactly what we have when we have a Microservices architecture. Using GraphQL on top of it all means we can reap the benefits from our chosen architecture at the same time as an App can get exactly the data it needs.

How you wonder? Stay with me throughout this article and you will see exactly how. Bring up your code editor cause you will build it with me :)

 The plan

Ok, so what are we building? It's always grateful to use an e-commerce company as the target as it contains so many interesting problems for us to solve. We will zoom in on two topics in particular namely products and reviews. When it comes to products we need a way to keep track of what products we sell and all their metadata. For reviews we need a way to offer our customer a way to review our products, give it a grade, a comment and so on and so forth. These two concepts can be seen as two isolated islands that can be maintained and developed independently. If for example, a product gets a new description there is no reason that should affect the review of said product.

Ok, so we turn these two concepts into product service and a review service.

Data structure for the services

What does the data look like for these services? Well they are in their infancy so let's assume the product service is a list of products for now with a product looking something like this:



[{
  id: 1,
  name: 'Avengers - infinity war',
  description: 'a Blue-ray movie'
}]


Enter fullscreen mode Exit fullscreen mode

The review service would also hold data in a list like so:



[{
  id: 2,
  title: 'Oh snap what an ending',
  grade: 5,
  comment: 'I need therapy after this...',
  product: 1
}]


Enter fullscreen mode Exit fullscreen mode

As you can see from the above data description the review service holds a reference to a product in the product service and it's by querying the product service that you get the full picture of both the review and the product involved.

 Dockerizing

Ok, so we understand what the services need to provide in terms of a schema. The services also need to be containerized so we will describe how to build them using Docker a Dockerfile and Docker Compose.

 GraphQL

So the GraphQL API serves as this high-level API that is able to combine results from our product service as well as review service. It's schema should look something like this:



type Product {
   id: ID,
   name: String,
   description: String
}

type Review {
  id: ID,
  title: String,
  grade: Int,
  comment: String,
  product: Product
} 

type Query {
  products: [Product]
  reviews: [Review]
 }


Enter fullscreen mode Exit fullscreen mode

We assume that when a user of our GraphQL API queries for Reviews they want to see more than just the review but also some extra data on the Product, what's it called, what it is and so on. For that reason, we've added the product property on the Review type in the above schema so that when we drill down in our query, we are able to get both Review and Product information.

 Serverless

So where does Serverless come into this? We need a way to host our API. We could be using an App Service but because our GraphQL API doesn't need to hold any state of its own and it only does a computation (it assembles a result) it makes more sense to make it a light-weight on-demand Azure Function. So that's what we are going to do :) As stated in the beginning we are saving this for the second part of our series, we don't want to bore you by a too lengthy article :)

Creating and Dockerizing our Microservices

We opt for making these services as simple as possible so we create REST APIs using Node.js and Express, like so:



/products
  app.js
  Dockerfile
  package.json
/reviews
  app.js
  Dockerfile
  package.json


Enter fullscreen mode Exit fullscreen mode

The app.js file for /products looks like this:



// products/app.js

const express = require('express')
const app = express()
const port = process.env.PORT || 3000

app.get('/', (req, res) => res.json([{
  id: "1",
  name: 'Avengers - infinity war',
  description: 'a Blue ray movie'
}]))
app.listen(port, () => console.log(`Example app listening on port port!`))


Enter fullscreen mode Exit fullscreen mode

and the app.js for /reviews looks like this:




// reviews.app.js

const express = require('express')
const app = express()
const port = process.env.PORT  || 3000

app.get('/', (req, res) => res.json([{
  id: "2",
  title: 'Oh snap what an ending',
  grade: 5,
  comment: 'I need therapy after this...',
  product: 1
}]))
app.listen(port, () => console.log(`Example app listening on port port!`))


Enter fullscreen mode Exit fullscreen mode

Looks almost the same right? Well, we try to keep things simple for now and return static data but it's quite simple to add database later on.

Dockerizing

Before we start Dockerizing we need to install our dependency Express like so:



npm install express


Enter fullscreen mode Exit fullscreen mode

This needs to be done for each service.

Ok, we showed you in the directory for each service how there was a Dockerfile. It looks like this:



// Dockerfile

FROM node:latest
WORKDIR /app
ENV PORT=3000
COPY . .
RUN npm install
EXPOSE $PORT
ENTRYPOINT ["npm", "start"]


Enter fullscreen mode Exit fullscreen mode

Let's go up one level and create a docker-compose.yaml file, so it's easier to create our images and containers. Your file system should now look like this:



docker.compose.yaml
/products
  app.js
  Dockerfile
  package.json
/reviews
  app.js
  Dockerfile
  package.json


Enter fullscreen mode Exit fullscreen mode

Your docker-compose.yaml should have the following content:



version: '3.3'
services: 
  product-service:
    build:
      context: ./products
    ports:
      - "8000:3000"
    networks: 
      - microservices
  review-service:
    build:
      context: ./reviews
    ports:
      - "8001:3000"
    networks:
      - microservices
networks: 
  microservices:


Enter fullscreen mode Exit fullscreen mode

We can now get our service up and running with



docker-compose up -d


Enter fullscreen mode Exit fullscreen mode

I always feel like I'm starting up a jet engine when I run this command as all of my containers go up at the same time, so here goes, ignition :)

You should be able to find the products service at http://localhost:8000 and the reviews service at http://localhost:8001. That covers the microservices for now, let's build our GraphQL API next.

Your products service should look like the following:

and your review service should look like this:

 Implementing GraphQL

There are many ways to build a GraphQL server, we could be using the raw graphql NPM library or the express-graphql, this will host our server in a Node.js Express server. Or we could be using the one from Apollo and so on. We opt for the first one graphql as we will ultimately serve it from a Serverless function.

So what do we need to do:

  1. Define a schema
  2. Define services we can use to resolve different parts of our schema
  3. Try out the API

Define a schema

Now, this is an interesting one, we have two options here for defining a schema, either use the helper function buildSchema() or use the raw approach and construct our schema using primitives. For this case, we will use the raw approach and the reason for that is I simply couldn't find how to resolve things at depth using buildSchema() despite reading through the manual twice. It's strangely enough easily done if we were to use express-graphql or Apollo so sorry if you feel your eyes bleed a little ;)

Ok, let's define our schema first:



// schema.js

const { 
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLInt,
  GraphQLNonNull,
  GraphQLList,
  GraphQLString
} = require('graphql');

const {
  getProducts,
  getReviews,
  getProduct
} = require('./services');

const reviewType = new GraphQLObjectType({
  name: 'Review',
  description: 'A review',
  fields: () => ({
    id: {
      type: GraphQLNonNull(GraphQLString),
      description: 'The id of Review.',
    },
    title: {
      type: GraphQLString,
      description: 'The title of the Review.',
    },
    comment: {
      type: GraphQLString,
      description: 'The comment of the Review.',
    },
    grade : {
      type: GraphQLInt
    },
    product: {
      type: productType,
      description: 'The product of the Review.',
      resolve: (review) => getProduct(review.product) 
    }
  })
})

const productType = new GraphQLObjectType({
  name: 'Product',
  description: 'A product',
  fields: () => ({
    id: {
      type: GraphQLNonNull(GraphQLString),
      description: 'The id of Product.',
    },
    name: {
      type: GraphQLString,
      description: 'The name of the Product.',
    },
    description: {
      type: GraphQLString,
      description: 'The description of the Product.',
    }
  })
});

const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    hello: {
      type: GraphQLString,
      resolve: (root) => 'world'
    },
    products: {
      type: new GraphQLList(productType),
      resolve: (root) => getProducts(),
    },
    reviews: {
      type: new GraphQLList(reviewType),
      resolve: (root) => getReviews(),
    }
  }),
});

module.exports = new GraphQLSchema({
  query: queryType,
  types: [reviewType, productType],
});


Enter fullscreen mode Exit fullscreen mode

Above we are defining two types Review and Product and we expose two query fields products and reviews.

I want you to pay special attention to the variable reviewType and how we resolve the product field. Here we are resolving it like so:



resolve: (review) => getProduct(review.product) 


Enter fullscreen mode Exit fullscreen mode

Why do we do that? Well, it has to do with how data is stored on a Review. Let's revisit that. A Review stores its data like so:



{
  title: ''
  comment: '',
  grade: 5
  product: 1
}


Enter fullscreen mode Exit fullscreen mode

As you can see above the product field is an integer. It's a foreign key pointing to a real product in the product service. So we need to resolve it so the API can be queried like so:



{
  reviews {
    product { 
      name
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

If we don't resolve product to a product object instead the above query would error out.

Create services

In our schema.js we called methods like getProducts(), getReviews() and getProduct() and we need those to exist so we create a file services.js, like so:



const fetch = require('node-fetch');

const getProducts = async() => {
  const res = await fetch(process.env.PRODUCTS_URL)
  const json = res.json();
  return json;
}

const getProduct = async(product) => {
  const products = await getProducts()
  return products.find(p => p.id == product);
} 

const getReviews = async() => {
  const res = await fetch(process.env.REVIEW_URL)
  const json = res.json();
  return json;
}

module.exports = {
  getProducts,
  getReviews,
  getProduct
}


Enter fullscreen mode Exit fullscreen mode

Ok, we can see above that methods getProducts() and getReviews() makes HTTP requests to URL, at least judging by the names process.env.PRODUCTS_URL and process.env.REVIEW_URL. For now, we have created a .env file in which we create those two env variables like so:



PRODUCTS_URL = http://localhost:8000
REVIEW_URL = http://localhost:8001


Enter fullscreen mode Exit fullscreen mode

Wait, isn't that? Yes, it is. It is the URLs to product service and review service after we used docker-compose to bring them up. This is a great way to test your Microservice architecture locally but also prepare for deployment to the Cloud. Deploying to the Cloud is almost as simple as switching these env variables to Cloud endpoints, as you will see in the next part of this article series :)

Trying out our GraphQL API

Ok, so we need to try our code out. To do that let's create an app.js file in which we invoke the graphql() function and let's provide it our schema and query, like so:



const { graphql } = require('graphql');
const rawSchema = require('./raw-schema');
require('dotenv').config()

const query = `{ hello products { name, description } reviews { title, comment, grade, product { name, description } } }`;

graphql({
  schema: rawSchema,
  source: query
}).then(result => {
  console.log('result', result);
  console.log('reviews', result.data.reviews);
})


Enter fullscreen mode Exit fullscreen mode

In the above code, we specify a query and we expect the fields hello, products and reviews to come back to us and finally we invoke graphql() that on the then() callback serves up the result. The result should look like this:

Summary

We set out on a journey that would eventually lead us to the cloud. We are not there yet but part two will take us all the way. In this first part, we've managed to create microservices and dockerize them. Furthermore, we've managed to construct a GraphQL API that is able to define a schema that merges our two APIs together and serve that up.

What remains to do, that will be the job of the second part, is to push our containers to the cloud and create service endpoints. When we have the service endpoints we can replace the value of environment variables to use the Cloud URLs instead of the localhost ones we are using now.

Lastly, we need to scaffold a serverless function but that's all in the next part so I hope you look forward to that :)

💖 💪 🙅 🚩
softchris
Chris Noring

Posted on April 13, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related