Unlock NGINX hidden powers with Javascript module (NJS)

Unlock NGINX hidden powers with Javascript module (NJS)

According to W3Techs Statistics, NGINX was the most popular HTTP Server until March 2022, while Javascript remains the language most-utilized language among its developers. I'm a big fan, so why not combine them?

Nothing new, guys, NGINX released the first NJS version in 2016, and He caught my attention. I tried to use it, but I found it difficult to get things to work because the project is still in the works, unlike the Lua module.

When you see the Javascript language inside the HTTP Server, you may believe that anything is possible.

The first thought that comes to mind is, why not create an API Gateway or Proxy? Kong is a project built on top of NGINX, they primarily use the Lua Module, but in the current version (2.8.x), you can also use Javascript, Python, and Go, which is very flexible.

Sometimes you need something lightweight that works without a lot of resources, such as JWT claims and validation, inject custom http header, log access, and monitoring.

Building a reverse proxy

The sample goal is to develop a proxy to handle API calls with validation; the great PokeAPI is used to supply some data because everyone loves Pokemon.

  • nginx/njs/poke.js
function authorizeApiKey(request) {
    if (_isValidApiKey(request)) return request.internalRedirect('@backend')

    request.return(401, 'Invalid api-key.')
}

function _isValidApiKey(request) {
    const apiKey = request.headersIn['api-key']

    if (!apiKey) {
        request.return(401, 'Missing api-key.')
        return false
    }

    return process.env.API_KEY == apiKey
}

export default { authorizeApiKey }

Responsible for verifying requests and forwarding them to the @backend route, where the proxy magic occurs. To keep things simple, the environment variable API_KEY defines the key between the client and the server.

  • nginx/nginx.conf
load_module modules/ngx_http_js_module.so;

events { }

http {
      js_path "/etc/nginx/njs/";

      js_import poke from poke.js;

      server {
            listen       80;
            listen  [::]:80;
            server_name  localhost;

            location / {
                js_content poke.authorizeApiKey;
            }

            location @backend {
                resolver 8.8.8.8;

                proxy_pass https://pokeapi.co/api/v2/$1$is_args$args;
                rewrite ^/(.*)$ $1 break;

                proxy_set_header Host             "pokeapi.co";
                proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
                proxy_set_header X-Real-IP         $remote_addr;

                proxy_ssl_server_name on;
            }
      }
}

As can be seen, two routes were created: a public "/" route where js_content is used to block access without a "api-key," and a private @backend route where proxy features are performed.

  • dockerfile
FROM nginx:1.23.2

ADD ./nginx/njs /etc/nginx/njs/
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf

We need to add asset files to container.

  • docker-compose.yml
version: '3.8'

services:
  nginx:
    build: ./
    restart: unless-stopped
    ports: 
      - 1234:80
    environment:
      API_KEY: "strong"

Compose file containing image build, exposed port 1234, and environment variables.

Guys, it's time to see how this thing works. Please enter the following commands.

docker-compose up -d

Let's check API without the api-key header.

curl --location --request GET 'http://localhost:1234/pokemon/charmander'
# > Missing api-key.

Testing with wrong api-key header.

curl --location --request GET 'http://localhost:1234/pokemon/charmander' \
--header 'api-key: none'
# > Invalid api-key.

So, it's time to receive some feedback.

curl --location --request GET 'http://localhost:1234/pokemon/charmander' \
--header 'api-key: strong'

Because the data response is so detailed, I need to trim and keep it simple.

{
  "name": "charmander",
  "id": 4,
  "height": 6,
  "weight": 85,
  "species": {
    "name": "charmander",
    "url": "https://pokeapi.co/api/v2/pokemon-species/4/"
  },
  "types": [
    {
      "slot": 1,
      "type": {
        "name": "fire",
        "url": "https://pokeapi.co/api/v2/type/10/"
      }
    }
  ]
}

If you want to learn more about API, check the documentation available.

Don't be afraid to clone the git repository 😜, https://github.com/williampsena/njs-poke-example.

I'm done!

Ready to create your Pokédex using NJS? Be aware that NJS is not Node.js and is compliant with ECMAScript 5.1 (strict mode) with some ECMAScript 6 and later extensions, so keep your kernel updated!

References