Managing microservices manually can get very complicated and takes the focus away from business logic. API gateways help take care of the various collective management and housekeeping tasks necessary for running microservices.
One of the most popular API gateways is Kong. As the illustration below shows, Kong takes care of the tedious tasks involved in managing and running microservices while leaving you to focus on building exciting features by leveraging on products like Stream Chat for live chats or Stream Activity Feeds for creating engaging activity feeds.
In this tutorial, we will be creating a live chat app using Stream Chat and a backend microservice exposed through Kong's API gateway.
Prerequisites
Before you proceed with this tutorial, make sure you have Docker, Node.js, and npm or yarn installed on your machine. We'll be building the frontend with Svelte and the backend with Express. Both are chosen because of their simplicity and so that you can understand and follow along even if this is your first encounter with them. Being minimalist tools, they don't distract from what's going on.
Sign up for Stream Chat
Visit this link to create a free Stream account or login to your existing account.
Once you’re logged in, you can use the app that's automatically created for you. Or, if you prefer, go to the dashboard, hit the blue “Create App” button at the top right of the screen and give your app a name as shown below:
In the App Access Keys section at the bottom, copy your Key and Secret for use later.
Kong Setup
We'll be setting up Kong using Docker to keep our local machine clean and free of dependencies, and using Docker ensures that the steps are the same for all platforms.
We need to create a Docker network for Kong and our microservices, then setup Kong's database where its configurations will be stored, and start-up Kong after that.
$ docker network create kong-net
$ docker run -d --name kong-database --network=kong-net \
-p 5432:5432 -e "POSTGRES_USER=kong" \
-e "POSTGRES_DB=kong" postgres:9.6
$ docker run --rm --network=kong-net -e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
kong:latest kong migrations bootstrap
$ docker run -d --name kong --network=kong-net -e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
-e "KONG_PROXY_ACCESS_LOG=/dev/stdout" -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
-e "KONG_PROXY_ERROR_LOG=/dev/stderr" -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
-e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" -p 8000:8000 -p 8443:8443 \
-p 8001:8001 -p 8444:8444 kong:latest
You can verify that Kong is running with:
$ curl -i http://localhost:8001/
For Kong to know about our microservice, we need to configure the microservice and tell Kong how to route requests to our app. But, we first have to create the microservice.
Setting up the Chat Microservice
Let's create our folder structure, initialize the project, and install the needed dependencies:
$ mkdir -p kongchat/{frontend,backend}
$ cd kongchat/backend
$ touch index.js
$ npm init -y
$ npm i express body-parser dotenv stream-chat ip
After that, create a file called index.js
in the backend directory and paste in the code below:
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const ip = require('ip');
const { StreamChat } = require('stream-chat');
const app = express();
// Add the middlewares
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Initialize our Stream Chat client
const streamClient = new StreamChat(process.env.KC_STREAM_CHAT_KEY, process.env.KC_STREAM_CHAT_SECRET);
We have two routes. The /token
endpoint accepts a username and generates a new token using the Stream Chat Node.js SDK, as we need to authenticate any new users before they can gain access. The /ping
endpoint is to check if the server is running.
Create a .env
file in the current directory (backend) and add your key and secret you copied from your Stream dashboard:
KC_STREAM_CHAT_KEY=xxxx
KC_STREAM_CHAT_SECRET=xxxx
In the scripts section of package.json
in the backend directory, add a start command to run the server as shown below:
{
"scripts": {
"start": "node index.js"
},
}
Start the server with:
$ npm start
Now that we've successfully written our backend microservice let's Dockerize it so Kong can manage it.
Add a Dockerfile in the backend directory and paste in:
FROM node:carbon-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 10000
CMD ["npm", "start"]
Then, in the root directory (kongchat) add a docker-compose.yml
file and paste in:
version: '3'
services:
kc_backend:
build: ./backend
image: kc_backend
container_name: kc_backend
network_mode: kong-net
With this, we can run our backend service using:
$ docker-compose up -d
Or, if you prefer it to run in the foreground:
$ docker-compose up
Registering Our Microservice With Kong
Now our back-end microservice is up and running; we need to tell Kong about it. To do so, we need to know its IP address on the kong-net
network. We can find that out by running:
$ docker network inspect kong-net
# Sample output
[
{
"Name": "kong-net",
"Id": "1176776a97a0d22a44789cdd8fb4408cb80e4af086ac6474ab503704fda06c40",
# ...
"Containers": {
# ... "3a21018c6c9a6c0877ac182e7cd0e3e3da0af87b36d1f2a4882f76fe43a03a27": {
"Name": "kc_backend",
"EndpointID": "8e7aec61796e745cff0e0e79d3e723b76dba52305c91d19aade6825a3e43d6e9",
"MacAddress": "02:42:ac:13:00:04",
"IPv4Address": "172.19.0.4/16",
"IPv6Address": ""
},
# ...
}
}
]
My microservice IP address, for example, is 172.19.0.4
, as seen from the IPv4Address
key of the Containers
field. I omitted the irrelevant parts of the output.
Now we have our IP address in hand; we register it with Kong by making a POST request to Kong's services endpoint:
$ curl -i -X POST --url http://localhost:8001/services/ --data 'name=kc-chat-backend' \
--data 'url=http://172.19.0.4' --data 'port=10000'
# Sample output
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/1.4.3
Content-Length: 295
X-Kong-Admin-Latency: 48
{
"host": "172.19.0.4",
"created_at": 1579439597,
Now Kong knows about our microservice. We then need to tell Kong what requests to route to our microservice:
$ curl -i -X POST --url http://localhost:8001/services/kc-chat-backend/routes \
--data 'paths[]=/api/kongchat' --data 'strip_path=false' --data 'methods[]=GET' \
--data 'methods[]=POST'
# Sample output
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/1.4.3
Content-Length: 407
X-Kong-Admin-Latency: 30
{
With that, we just told Kong to route GET
and POST
requests made to /api/kongchat
to our microservice.
We can check that Kong is correctly routing our API requests to our microservice by sending a request to /ping
:
$ curl -i -X GET --url http://localhost:8000/api/kongchat/ping
# Sample output
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 18
Connection: keep-alive
X-Powered-By: Express
ETag: W/"12-6FyCUNJCdUkgXM8yXmM99u6fQw0"
X-Kong-Upstream-Latency: 22
X-Kong-Proxy-Latency: 1
Via: kong/1.4.3
{
"message":"pong"
}
Since we'll be communicating with Kong from the browser, we need to turn on the CORS plugin for our service:
$ curl -X POST http://kong:8001/services/kc-chat-backend/plugins \
--data "name=cors" \
--data "config.origins=*" \
--data "config.methods=GET" \
--data "config.methods=POST" \
--data "config.methods=OPTIONS" \
--data "config.headers=Accept" \
--data "config.headers=Accept-Version" \
--data "config.headers=Content-Length" \
--data "config.headers=Content-MD5" \
--data "config.headers=Content-Type" \
--data "config.headers=Host" \
--data "config.headers=Date" \
--data "config.headers=X-Auth-Token" \
--data "config.exposed_headers=X-Auth-Token" \
--data "config.credentials=true" \
--data "config.max_age=3600"
Creating Our Frontend
In the frontend directory, we created earlier, run:
npx degit sveltejs/template .
npm i # install
npm i stream-chat dotenv
npm i -D postcss postcss-load-config svelte-preprocess tailwindcss @fullhuman/postcss-purgecss @rollup/plugin-replace # development dependencies
npx tailwind init
touch postcss.config.js
cp ../backend/.env . # Make a copy of backend's .env in frontend
npm run dev # and visit http://localhost:5000 in the browser
We're using Tailwind CSS for our styling. So let's integrate it into our development pipeline.
In postcss.config.js
, paste in:
const purgecss = require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.svelte', './src/**/*.html'],
whitelistPatterns: [/svelte-/],
defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
})
module.exports = {
plugins: [
require('tailwindcss'),
...(!process.env.ROLLUP_WATCH ? [purgecss] : [])
]
};
Replace the contents of rollup.config.js
with:
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import replace from '@rollup/plugin-replace';
import { config } from 'dotenv';
config();
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/main.js',
output: {
Our changes complete the setup of our CSS pre-processing and inject our environment variables for use in the app.
Modify the style tag of App.svelte
as below:
<style global>
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>
That's all for our CSS pre-processing. Let's dive into the mechanism of connecting to our proxied microservice.
When a user first loads the app, we want to show the login page below:
When the user types a username and clicks on Sign in, we want to show the chat window like the example below:
The contents of our App.svelte
is shown below:
<script>
import { onMount } from 'svelte';
import { StreamChat } from 'stream-chat/dist/index.js';
export let appName;
let loggedIn = false;
let online = false;
let token = '';
let username = '';
let message = '';
let messages = [];
let channel = null;
let user = null;
let streamClient = null;
let pingInterval = 30000; // 30 seconds
Let's go through the code.
At the beginning of our component's template, we have the app name and an indicator that shows if our microservice is up and running:
<h1 class="text-4xl text-center">
{appName}
</h1>
{#if online}
<span class="bg-green-500 rounded-full h-5 w-5 flex mx-auto"></span>
{:else}
<span class="bg-red-500 rounded-full h-5 w-5 flex mx-auto"></span>
{/if}
We call the /ping
endpoint we created on the microservice using pingService()
at intervals determined by pingInterval
.
When the user first opens the app (s)he is not logged in (loggedIn
is false) so we show the login form:
{#if loggedIn}
<!-- -->
{:else}
<div class="w-full mx-auto max-w-xs">
<form on:submit|preventDefault={joinChat} class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
Username
</label>
<input bind:value={username} class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="username" type="text" placeholder="Username">
</div>
<div class="flex items-center justify-between">
<button class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
Sign In
</button>
</div>
</form>
</div>
{/if}
Submitting the login form will call joinChat()
which sends a request to the Kong gateway to get a token. When the token is returned, we initialize Stream Chat (initializeStreamChat()
) and our messaging channel (initializeChannel()
), then we set loggedIn
to true, retrieve previous messages (if any) and register a listener for new messages to the channel.
We then show the chat window for logged in users:
{#if loggedIn}
<div class="container mx-auto">
<h5>You are <strong>{user.username}</strong></h5>
<br>
{#each messages as message}
<div class="w-full max-w my-1">
<div class="border border-gray-400 bg-white rounded p-4 flex flex-col justify-between leading-normal">
<div class="mb-8">
<p class="text-gray-700 text-base">{message.text}</p>
</div>
<div class="flex items-center">
<div class="text-sm">
<p class="text-gray-900 leading-none">By {message.user.id === user.username ? 'You' : message.user.name}</p>
</div>
When the Send
button is clicked, we publish the contents of message
to the channel and empty it after that, and Stream Chat ensures that all registered clients get the newly published message.
Demo
Open the app in different browser windows, log in with different usernames, and you can send messages between the logged-in users as shown in the images below:
Conclusion
You can find out more about Stream Chat and Kong by going through their documentation. The source code for this tutorial is hosted on GitHub.