Como prometido, nesse artigo eu vou ensinar a como criar uma API Gateway do zero com Node e Express.

Se você não souber node, não tem problema. A ideia é mostrar como uma API Gateway deve se comportar, e o código está todo documentado.

Para não ficar um post muito extenso vou dividir em duas partes. Nessa primeira parte, vamos criar o proxy único que irá receber as informações da requisição, encontrar o serviço e replicar a solicitação.

No próximo artigo vamos incluir um serviço exclusivo de segurança com ACL.

Então, mãos a obra, pois temos muito a estudar.

Pré-requisitos

  • node
  • npm
  • visual studio code
  • insomnia ou postman

Iniciando o projeto

Assumindo que você já está no diretório com o projeto iniciado (npm init -y), vamos instalar as dependências necessárias:

npm i express dotenv body-parser cors

Express: o Express é o framework que vamos usar para criar nossa gateway. Ele é bem enxuto e simples, por isso escolhi ele.

Dot Env: esse pacote lê o arquivo .env na raiz do projeto. Vamos usar esse arquivo para armazenar algumas variáveis de ambiente.

Body Parser: faz o parse de requisições e adiciona ao req.body.

CORS: habilita comunicação cross origin (política de privacidade).

Estrutura

Na raiz do seu projeto, adicione um arquivo chamado server.js, e alguns outros arquivos e diretórios:

+- src
|  |- helper/
|  |- middleware/
|  |- app.js
|  |- routes.js
|  +- services.map.js
|- .env
|- .gitignore
+- server.js

Sem tempo para perguntas, você vai entender cada um deles com o tempo.

Aplicação

Vamos iniciar a nossa aplicação em express. No arquivo src/app.js, colamos o código:

const express = require('express')
const routes = require('./routes')
const bodyParser = require('body-parser')

// starting a express app
const app = express()

// bodyparser middleware
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

// installing routes in root endpoint
app.use('/', routes)

module.exports = app

No script acima, a gente só inicializa um app em express, importa as rotas e exporta o módulo app, para acessar posteriormente.

Antes de configurar o server.js, vamos criar um "Hello, World" básico, só pra verificar o funcionamento da nossa API.

No arquivo src/routes.js criamos nossa função helloWorld diretamente:

const router = require('express').Router()

const helloWorld = (req, res) => {
    return res.send('Hello, World')
}

router.get('/', helloWorld)

module.exports = router

Agora sim, vamos iniciar nosso servidor com esse script no server.js:

// read .env and add it to process.env variable
require('dotenv').config()

// importing express app
const app = require('./src/app')

// clear console and expose app
console.clear()
app.listen(process.env.PORT || 4000, (err) =>
    err
        ? console.log('erro')
        : console.log('API iniciada')
)

Pronto, agora você pode rodar um node server.js no terminal, e, se seguiu corretamente os passos, ao acessar localhost:4000 você receberá um Hello, World como resposta.

Mas nossa Gateway não é esse Hello World. A gente fez ele só pra verificar se está tudo OK. Agora chegou a hora de fazer com que ela vire um proxy.

Criando um proxy

Uma API Gateway é uma camada antes do seu serviço, e isso faz com que ela vire, automaticamente, um proxy.

A ideia agora é receber a requisição, fazer a tratativa necessária e replicar a requisição para o serviço. Na essência, a API Gateway só gerencia requisições.

Então, sem enrolação, vamos ao desenvolvimento.

Services mapping

Pra facilitar a adição de novos serviços, vamos abstrair tudo para um arquivo de mapping. Posteriormente, esses dados virão do banco de dados, mas, por hora, vamos inputar os dados hard coded mesmo.

No arquivo src/services.map.js vamos criar um array de serviços.

Um serviço vai ter algumas coisas em comum: o host, a porta, e o protocolo (http ou https). Em JavaScript, podemos representar nosso serviço como:

{
    "host": "127.0.0.1",
    "port": 3333,
    "https": false
}

Com isso a gente consegue montar a URL de requisição, certo?

Vamos adicionar mais 2 propriedades, para melhorar o controle interno da nossa API Gateway: "name" e "prefix".

  • name Será o nome que vamos dar ao serviço, para facilitar a depuração e logs;
  • prefix Será o prefixo para quem for requisitar aquele serviço externamente.

O prefix é necessário pois nosso serviço deve ser independente, e não vamos adicionar prefixos de rota diretamente nele.

{
    "host": "127.0.0.1",
    "port": 3333,
    "https": false,
    "name": "Postagens",
    "prefix": "posts"
}

Esse serviço roda local (127.0.0.1), na porta 3333 e sua rota index (/) já trabalha sobre os posts, pois esse serviço só gerencia isso.

É uma má prática prefixar diretamente nos serviços. Isso tira o poder de gerenciamento da API Gateway, migração e implementação de betas. Além de que não faz sentido dizer que há um nó de postagens, no serviço que só gerencia postagens.

Então, se o usuário fizer a requisição para https://api.com/posts/1234 a gateway deverá saber que a requisição interna deverá ser http://127.0.0.1:3333/1234.

Entendido o conceito, faça o mapping de alguns serviços que você tem na sua máquina.

// src/services.map.js
const services = [
  {
    name: 'Posts',
    prefix: 'posts',
    host: '127.0.0.1',
    port: '4567',
    https: false
  },
  {
    name: 'Tweets',
    prefix: 'tweets',
    host: '127.0.0.1',
    port: '3333',
    https: false
  }
]

module.exports = services

Se você não tiver nenhum serviço que atenda os requisitos, baixe os de exemplo no GitHub.

Routing register

Depois de mapeado os serviços, é hora de registrar as rotas, e vamos automatizar isso com um Helper.

Crie um arquivo routeRegister.js em src/helpers, e vamos começar a entender a magia.

Precisamos de uma função que seja capaz de ler todos os serviços in memory e criar as rotas, e mapear uma função de proxy nela.

  1. Instale o Axios:
npm i axios
  1. Cole a função de mapping:

OBS: Eu documentei o script inteiro em português para facilitar. Leia ele para entender o que acontece em detalhes.

const http = require('axios')
const auth = require('../middlewares/auth')

/**
 * Recupera a URL do serviço *SEM* o prefixo
 * @param {object} service Serviço
 */
const getUrlFromService = ({ host, port, https, prefix }) => `${https ? 'https' : 'http'}://${host}${port ? `:${port}` : ''}`

/**
 * Registra todas as rotas de serviços.
 * ========================
 * @param {object} services serviços a serem registrados
 * @param {object} router objeto router do express
 */
const register = (services, router) => {
  services.map(service => {
    // URL do serviço que a Gateway vai requisitar
    const serviceUrl = getUrlFromService(service)

    // Registra a rota externa com o prefixo
    router.all(`/${service.prefix}*`, auth, async function (req, res) {
      const { originalUrl } = req

      // Remove o prefixo da URL acionada pelo cliente
      const url = serviceUrl + originalUrl.replace(`/${service.prefix}`, '')

      // método, cabeçalhos e corpo enviados pelo cliente
      const method = req.method.toLowerCase()
      const headers = req.headers
      const data = req.body

      // status padrão
      let status = 200

      // requisita o serviço indicado, com os mesmos cabeçalhos, métodos, e corpo
      await http({
          url,
          method,
          headers,
          data
        })

        // Se tudo der certo replica a resposta do serviço para o usuário
        .then(response => {
          res.status(response.status).send(response.data)
        })

        // Se deu algum erro
        .catch(error => {
          // caso haja uma resposta do serviço, replica a resposta
          if (error.response) {
            const { response } = error
            status = response.status
            res
              .status(status)
              .send(response.data)
          }
          // caso não haja uma resposta do serviço, cria o erro
          else {
            status = 500
            res
              .status(status)
              .send({ message: `Não foi possível se conectar ao serviço ${service.name}` })
          }
        })
    })
  })
}

module.exports = register

Por fim, vamos executar essa função no nosso arquivo src/routes.js

const { Router } = require('express')
const services = require('./services.map')
const routerRegister = require('./helpers/routerRegister')
const router = Router()

// register all services proxies
routerRegister(services, router)

module.exports = router

E agora sim, temos uma API Gateway funcional.

O próximo passo é centralizar a autenticação, mas isso vamos deixar para o próximo artigo, pois esse está muito grande já.

Um abraço, até a próxima.