├── .gitignore ├── License.md ├── README.md ├── api-gateway ├── Dockerfile ├── app.js ├── bin │ └── www ├── lib │ ├── proxy.js │ └── restreamer.js ├── middleware │ └── SayHello.js ├── package.json └── services.json ├── docker-compose.yml └── hello-world-api ├── Dockerfile ├── app.js ├── bin └── www └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | */node_modules/ 2 | */*.log 3 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 DANIEL K. ARELLANO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodejs-docker-api-gateway-example 2 | Example of a lightweight and extensible NodeJS based API gateway implementation using Docker. 3 | 4 | # Requirements 5 | - Docker 6 | - Docker-compose version 1.6.0+ 7 | 8 | # Deploying locally 9 | - Clone the repo 10 | - Ensure docker daemon is running 11 | - ```cd nodejs-docker-api-gateway-example/``` 12 | - ```docker-compose up -d``` 13 | - The api-gateway and hello-world-api should now be running and accessible at [localhost:3000](http://localhost:3000) and [localhost:3000/api/helloapi/hello](http://localhost:3000/api/helloapi/hello) 14 | - Note: If you want to access the hello-world-api directly: 15 | - ```docker ps``` 16 | - Take note of the port that the hello-world-api is running on, i.e. in ```0.0.0.0:32771->3000/tcp``` it would be 32771 17 | - Go to [localhost:/api/v1/hello](http://localhost:/api/v1/hello) 18 | 19 | # Deploying in Swarm Mode 20 | Deploying in Docker Swarm Mode will vary based on your setup however you can follow the general guidelines below assuming you have a swarm set up and your docker engine is pointing to that swarm: 21 | - Build the api-gateway and hello-world-api images 22 | - Push those images to your docker swarm's image repository 23 | - Create a network on your docker swarm 24 | - ```docker network create my-net --driver overlay``` 25 | - Deploy hello-world-api to the swarm using the network you created 26 | - ```docker service create --name helloapi --replicas 1 --network my-net /helloapi``` 27 | - Deploy api-gateway to the swarm using the network you created 28 | - ```docker service create -p 3000:3000 --name api-gateway --replicas 1 --network my-net /api-gateway``` 29 | - Note: api-gateway should expose a port accessible through the swarm i.e. ```-p 3000:3000``` 30 | 31 | 32 | # Documentation 33 | In this section we will explore how the API Gateway works and how it can be used. 34 | 35 | ### The Hello World API 36 | The Hello World API is just a simple Express-based REST API that exposes the endpoint "/api/v1/hello" to all HTTP methods. 37 | 38 | The Hello World API is comprised of 2 files: 39 | - bin/www 40 | - This file imports app.js, sets up the server and runs the server 41 | - app.js 42 | - This file sets up the Express-based app with a single REST endpoint "/api/v1/hello" available to all HTTP methods 43 | 44 | 45 | The following code snippet shows the REST endpoint exposed by the service: 46 | ```javascript 47 | app.all('/api/v1/hello', function(req, res) { 48 | const response = { 49 | message: 'hello', 50 | query: req.query, 51 | body: req.body, 52 | }; 53 | 54 | let gatewayMsg = req.headers['gateway-message']; 55 | if(gatewayMsg) { 56 | response['gateway-message'] = JSON.parse(gatewayMsg); 57 | } 58 | 59 | res.json(response); 60 | }); 61 | ``` 62 | As you can see, it creates a resonse comprised of the request's query paramaters, body (for POST requests) and a message 'hello'. 63 | It also checks to see if there is an HTTP header 'gateway-message' and appends it to the response if so. 64 | 65 | ### The API Gateway 66 | At it's core, the API Gateway is a simple Express-based REST API that acts as a reverse proxy that is extensible and lets you manipulate or react to requests before they reach their destination. 67 | It allows the use of middleware to add functionality and makes use of the NPM module [http-proxy](https://www.npmjs.com/package/http-proxy) to redirect requests to their destination. 68 | 69 | The API Gateway is comprised mainly by the following 3 files: 70 | - bin/www 71 | - This file imports app.js, sets up the server and runs the server 72 | - app.js 73 | - This file imports the settings from services.json in order to bootstrap the proxy. 74 | - services.json 75 | - This file is where you define your services and what middleware they should use 76 | 77 | #### The Bootstrapping Process (app.js) 78 | As mentioned above, the purpose of app.js is to set up the server to proxy requests. It does this by reading in the contents of services.json which should contain an array of services (as described below) and then configuring a REST endpoint at "/api/{service name}" 79 | 80 | For example: The name of our Hello World API is "helloapi", thus when going through the API Gateway we will access the Hello World API through the endpoint "/api/helloapi" 81 | 82 | 83 | #### Configuring Your Services (services.json) 84 | This is where you define your services. It should contain a JSON array of objects each corresponding to an individual service. This object should contain the following parameters: 85 | - name 86 | - The name of your service. This is used to set up the endpoint on the API Gateway that will redirect requests to the actual service. 87 | - host 88 | - What host to proxy the requests to 89 | - port 90 | - What port to proxy the requests to 91 | - protocol 92 | - what protocol to use when proxying the request (default: http) 93 | - rootPath 94 | - The root path to proxy requests to on the service (default: "") 95 | - For example: 96 | - The rootPath of the Hello World API is "api/v1". If we omit this from our services.json file then in order to reach the "api/v1/hello" endpoint on the Hello World API from the API Gateway we would have to use the endpoint "/api/helloapi/api/v1/hello" instead of "/api/helloapi/hello" 97 | - middleware 98 | - An array of strings corresponding to the name of a javascript file inside the "middleware/" directory. This is how you specify what middleware a service should use. 99 | - Note: middleware will be applied in the order that it appears in the array 100 | 101 | #### Middleware (middleware/*.js) 102 | The "middleware/" directory is used to hold your custom Express middleware to apply onto the requests before they are proxied. For example, let's take a look at the example middleware "middleware/SayHello.js": 103 | ```javascript 104 | module.exports = function(req, res, next) { 105 | const message = { message: 'Hello from the API Gateway!' }; 106 | req.headers['gateway-message'] = JSON.stringify(message); 107 | next(); 108 | }; 109 | ``` 110 | The role of this middleware is to append the HTTP header "gateway-message" to the request before it is proxied. 111 | 112 | If you are unfamiliar with middleware or how it is used in NodeJS/Express I highly recommend reading [Using Express middleware](http://expressjs.com/en/guide/using-middleware.html) or doing some research on your on. 113 | 114 | ### Docker 115 | Discussing what Docker is and how it works is beyond the scope of this tutorial, however in this section I will briefly discuss why Docker works well with this implementation of an API Gateway. 116 | 117 | Docker's networking features whether on a single machine using [Docker Compose](https://docs.docker.com/compose/) or on a [Docker Swarm](https://docs.docker.com/engine/swarm/) allow you to use simple "host" names when defining your services in the API Gateway's "services.json" file. This is because Docker allows you to access other containers on the same [Docker netowork](https://docs.docker.com/engine/userguide/networking/) via their container/service name. 118 | 119 | For example: 120 | In our "docker-compose.yml" file we called our components "gateway" and "helloapi". This allows us to use the host name "helloapi" in our "services.json" file because requests to "helloapi" from "gateway" will automatically route to the appropriate Docker container running "helloapi". 121 | 122 | When running on a Docker swarm, where you can have multiple instances of the same service, this comes with the added benefits of automatically load balancing your requests across those instances. 123 | 124 | ### Summary 125 | This implementation of an API Gateway is meant to illustrate how you can easily build a flexible yet powerful API Gateway using NodeJS/Express and Docker. In short, this implementation uses Express middleware and the NPM module [http-proxy](https://www.npmjs.com/package/http-proxy) to create a reverse proxy in which you can add functionality to modify and react to requests before they are sent to their intended destination. 126 | 127 | #### Disclaimer 128 | I would not recommend using this exact implementation in a critical production enviroment since it has not yet been thoroughly tested in such an environment, however you are free to use and modify the code as you wish. 129 | 130 | #### Final Thoughts / Future Development 131 | - It may be worthwile to use something like [Apache Zookeeper](https://zookeeper.apache.org/) to configure the services instead of the "services.json" file. 132 | - Other types of protocols aside from "http" and "https" still need to be tested. I am especially interested in seeing if I am able to proxy a request to connect to a websocket server or other two-way communication servers and maintain that connection between the client/server. 133 | -------------------------------------------------------------------------------- /api-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | ADD bin/ /app/bin 4 | ADD lib/ /app/lib 5 | ADD middleware/ /app/middleware 6 | ADD app.js /app/app.js 7 | ADD package.json /app/package.json 8 | ADD services.json /app/services.json 9 | 10 | WORKDIR /app 11 | 12 | RUN npm install 13 | 14 | EXPOSE 3000 15 | 16 | CMD npm start 17 | -------------------------------------------------------------------------------- /api-gateway/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require("body-parser"); 3 | const cookieParser = require('cookie-parser'); 4 | const url = require('url') 5 | const path = require('path'); 6 | const logger = require('morgan'); 7 | const services = require('./services.json'); 8 | const proxy = require('./lib/proxy'); 9 | const restreamer = require('./lib/restreamer'); 10 | 11 | // set up the app 12 | const app = express(); 13 | app.use(logger('dev')); 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({ extended: true })); 16 | app.use(cookieParser()); 17 | 18 | app.get('/', function(req, res) { 19 | res.json({ 20 | message: "API Gateway is alive." 21 | }); 22 | }); 23 | 24 | // Bootstrap services 25 | for(let i = 0; i < services.length; i++) { 26 | const name = services[i].name; 27 | const host = services[i].host; 28 | const port = services[i].port; 29 | const rootPath = services[i].rootPath || ""; 30 | const protocol = services[i].protocol || "http"; 31 | 32 | console.log(`Boostrapping service: ${protocol}://${host}:${port}/${rootPath}`); 33 | 34 | let middleware = []; 35 | if(services[i].middleware) { 36 | middleware = services[i].middleware.map(text => require(`./middleware/${text}`)); 37 | } 38 | 39 | // need to restream the request so that it can be proxied 40 | middleware.push(restreamer()); 41 | 42 | app.use(`/api/${name}*`, middleware, (req, res, next) => { 43 | const newPath = url.parse(req.originalUrl).pathname.replace(`/api/${name}`, rootPath); 44 | console.log(`Forwarding request to: ${newPath}`); 45 | proxy.web(req, res, { target: `${protocol}://${host}:${port}/${newPath}` }, next); 46 | }); 47 | } 48 | 49 | module.exports = app; 50 | -------------------------------------------------------------------------------- /api-gateway/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const app = require('../app'); 8 | const debug = require('debug')('api-gateway:server'); 9 | const http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | const port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | const server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | const port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | const bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | const addr = server.address(); 86 | const bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /api-gateway/lib/proxy.js: -------------------------------------------------------------------------------- 1 | const httpProxy = require('http-proxy'); 2 | const proxy = httpProxy.createProxyServer(); 3 | module.exports = proxy; 4 | -------------------------------------------------------------------------------- /api-gateway/lib/restreamer.js: -------------------------------------------------------------------------------- 1 | // NOTE: this is modified from https://github.com/dominictarr/connect-restreamer 2 | 3 | module.exports = function (options) { 4 | options = options || {} 5 | options.property = options.property || 'body' 6 | options.stringify = options.stringify || JSON.stringify 7 | 8 | return function (req, res, next) { 9 | if(req.method==="POST") { 10 | req.removeAllListeners('data') 11 | req.removeAllListeners('end') 12 | if(req.headers['content-length'] !== undefined){ 13 | req.headers['content-length'] = Buffer.byteLength(options.stringify(req[options.property]), 'utf8') 14 | } 15 | 16 | process.nextTick(function () { 17 | if(req[options.property]) { 18 | if('function' === typeof options.modify) 19 | req[options.property] = options.modify(req[options.property]) 20 | req.emit('data', options.stringify(req[options.property])) 21 | } 22 | req.emit('end') 23 | }); 24 | } 25 | next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api-gateway/middleware/SayHello.js: -------------------------------------------------------------------------------- 1 | module.exports = function(req, res, next) { 2 | const message = { message: 'Hello from the API Gateway!' }; 3 | req.headers['gateway-message'] = JSON.stringify(message); 4 | next(); 5 | }; 6 | -------------------------------------------------------------------------------- /api-gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-gateway", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.15.2", 10 | "connect-restreamer": "^1.0.3", 11 | "cookie-parser": "~1.4.3", 12 | "debug": "~2.2.0", 13 | "express": "~4.14.0", 14 | "http-proxy": "^1.15.2", 15 | "morgan": "~1.7.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api-gateway/services.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "helloapi", 4 | "host": "helloapi", 5 | "port": "3000", 6 | "protocol": "http", 7 | "rootPath": "api/v1", 8 | "middleware": [ 9 | "SayHello" 10 | ] 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Local Deployment 2 | version: "2" 3 | services: 4 | gateway: 5 | build: ./api-gateway/ 6 | ports: 7 | - "3000:3000" 8 | helloapi: 9 | build: ./hello-world-api/ 10 | ports: 11 | - "3000" 12 | -------------------------------------------------------------------------------- /hello-world-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | ADD bin/ /app/bin 4 | ADD app.js /app/app.js 5 | ADD package.json /app/package.json 6 | 7 | WORKDIR /app 8 | 9 | RUN npm install 10 | 11 | EXPOSE 3000 12 | 13 | CMD npm start 14 | -------------------------------------------------------------------------------- /hello-world-api/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const bodyParser = require('body-parser'); 5 | 6 | const app = express(); 7 | 8 | app.use(logger('dev')); 9 | app.use(bodyParser.json()); 10 | app.use(bodyParser.urlencoded({ extended: false })); 11 | 12 | app.get('/', function(req,res){ 13 | res.json({ 14 | hello: 'world!' 15 | }); 16 | }); 17 | 18 | app.all('/api/v1/hello', function(req, res) { 19 | const response = { 20 | message: 'hello', 21 | query: req.query, 22 | body: req.body, 23 | }; 24 | 25 | let gatewayMsg = req.headers['gateway-message']; 26 | if(gatewayMsg) { 27 | response['gateway-message'] = JSON.parse(gatewayMsg); 28 | } 29 | 30 | res.json(response); 31 | }); 32 | 33 | module.exports = app; 34 | -------------------------------------------------------------------------------- /hello-world-api/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const app = require('../app'); 8 | const debug = require('debug')('hello-word-api:server'); 9 | const http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | const port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | const server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | const port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | const bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | const addr = server.address(); 86 | const bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /hello-world-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-word-api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.15.2", 10 | "debug": "~2.2.0", 11 | "express": "~4.14.0", 12 | "morgan": "~1.7.0" 13 | } 14 | } 15 | --------------------------------------------------------------------------------