Learn how YOU can build a Serverless GraphQL API on top of a Microservice architecture, part I
Chris Noring
Posted on April 13, 2019
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:
- Building Microservices and a GraphQL API, Part I, we are here
- Hosting the GraphQL API in a Serverless app and bring it all to the Cloud, part II
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:
- 5 part series on Docker, This covers the Basics of Docker, It includes Dockerfiles, containers, images and Docker Compose
- 3 part series on GraphQL, This covers the basics on GraphQL
- Free Azure Account To be able to Deploy your services and host your Serverless function, you will need a free Azure account
- Tutorial on Azure functions with VS Code, This post covers how to create serverless functions using VS Code
- Github repo with code to create a GraphQL API and a Serverless function This article series is heavily inspired by the workshop created by my colleague Simona Cotin
- Containers in the Cloud, as we are about to put our containers in the Cloud in the next part in this series it can be a good idea to read up on all the offerings of Containers in the Cloud
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'
}]
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
}]
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]
}
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
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!`))
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!`))
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
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"]
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
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:
We can now get our service up and running with
docker-compose up -d
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:
- Define a schema
- Define services we can use to resolve different parts of our schema
- 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],
});
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)
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
}
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
}
}
}
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
}
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
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);
})
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 :)
Posted on April 13, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.