DEV Community

Larry Kipkemoi
Larry Kipkemoi

Posted on

Extending NGINX with JavaScript (NJS)

NGINX is a free open source web server that is used as a high performance reverse proxy, load balancer and more. Since its public release in 2004 it has powered most of the worlds websites.
Have you ever wanted to extend NGINX with custom logic, like authentication or caching rules. Extending the web server can be done using Lua, C or NJS.

Why extend NGINX.

  1. Custom logic and behavior
  2. Performance optimization like load balancing and caching
  3. Security Enhancement eg through rate limiting

In this guide, I'll do the following:

  1. Run JavaScript inside NGINX using the NJS module
  2. Create a basic API gateway
  3. Add authentication and custom headers

NJS SCRIPTING

JS support for NGINX is through the NJS module, which was first introduced in 2016. There are two js modules: ngx_http_js_module and ngx_stream_js_module. On this article I will be using the ngx_http_js_module.

I will be building a basic/primitive api gateway using nginx, with no auth server. Data will be on the web server, just for learning purposes.

Prerequisites

  • Basic NGINX knowledge
  • NGINX and njs => 0.9 installed

    • Check versions and install if not installed:
    nginx -v
    njs -v
    

BASIC NGINX CONFIG

The following is a basic NGINX config file that returns hello world:
This is config listens to port 8080 and returns status 200 and hello world.

# /etc/nginx/nginx.conf

events {}

http {

    server {
        listen 8080;
        server name _;

        location / {
            return 200 "hello world"
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Then run

sudo nginx -t #to test the file
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

USING NJS

The NJS module has several directives that can be used in the config file. These help NGINX deal with JS files eg importing, reading etc.
Let's look at these:

Importing and response

  • js_path: used to set the path for the njs files
  • js_import: imports the file
  • and js_content: used to set content from a js function to the response

Let's create our test js file

// /etc/nginx/njs/hello.js
function sayHello(r) {
  r.return(200, "hello there");
}
export default { sayHello };
Enter fullscreen mode Exit fullscreen mode

Importing and using it in the config:

# /etc/nginx/nginx.conf
load_module modules/ngx_http_js_module.so;

events {}

http {
        js_path /etc/nginx/njs/; #sets the path
        js_import hello from hello.js; #imports file as hello

        server {
        listen 8080;
        server_name _;

        location / {
            js_content hello.sayHello; #sets content response
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Test the file and reload the server.

When we make a request to the server:

> curl localhost:8080
hello there
Enter fullscreen mode Exit fullscreen mode

Adding a Proxy

Instead of just sending hello world, lets proxy to jsonplaceholder.typecode.com
So lets create a new path for this:

# /etc/nginx/nginx.conf
load_module modules/ngx_http_js_module.so;

#include /etc/nginx/modules-enabled/*.conf;

events {}

http {
        js_path /etc/nginx/njs/; #sets the path
        js_import hello from hello.js; #imports file as hello

        server {
        listen 8080;
        server_name _;

        location / {
            js_content hello.sayHello;
        }

        location /jph/ {
                resolver 1.1.1.1; #using Cloudflare DNS resolver
                proxy_pass https://jsonplaceholder.typicode.com/;
                proxy_ssl_server_name on;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

When we make a request:

> curl localhost:8080/jph/posts/1
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Enter fullscreen mode Exit fullscreen mode

Using auth_request Module

Nice!

But we do not want to give any unauthorized persons the ability to use this endpoint. It is free but I am gatekeeping.

We can use the auth_request directive from NGINX. This is used for authentication. I will use inline comments for more elaboration. (It is not a JS module though.)

# /etc/nginx/nginx.conf
load_module modules/ngx_http_js_module.so;

events {}

http {
        js_path /etc/nginx/njs/; #sets the path
        js_import hello from hello.js; #imports file as hello
        js_import auth from auth.js;
        js_shared_dict_zone zone=apikeys:1M; #creates a share memory zone which all nginx workers can access

        server {
        listen 8080;
        server_name _;

        location / {
            js_content hello.sayHello;
        }

        location /jph/ {
                auth_request /auth; #uses location /auth to authenticate request
                resolver 1.1.1.1; #using Cloudflare DNS resolver
                proxy_pass https://jsonplaceholder.typicode.com/;
                proxy_ssl_server_name on;
        }

        location = /auth {
                internal; # makes the location internal to nginx only
                js_content auth.doAuth; #calls the doAuth function
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Implementing the auth.js file:

// /etc/nginx/njs/auth.js
//loading the shared object. ngx is a global nginx provided variable
const store = ngx.shared.apikeys;

//create demo user
if (!store.get("demo")) {
  store.set(
    "demo",
    JSON.stringify({
      routes: ["/api/v1/*"],
      credit: 15,
      name: "demo",
    })
  );
}

function doAuth(r) {
  const key = r.headersIn["x-api-key"]; //get the key from the request header
  if (!key) {
    r.return(401, "missing key");
    return;
  }
  const user = store.get(key);
  if (!user) {
    r.return(401, "key not found");
    return;
  }
  r.return(200);
}
export default { doAuth };
Enter fullscreen mode Exit fullscreen mode

When we make requests:

$ curl localhost:8080/jph/post/1 #without header or wrong header
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.28.0</center>
</body>
</html>

$ curl -H 'x-api-key: demo' localhost:8080/jph/posts/1 #with header
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Enter fullscreen mode Exit fullscreen mode

Awesome! Now we have a shared memory zone to store our api keys. The js_shared_dict_zone is a directive that takes some options. I have use the zone=name:size option. This zone is accessible in all the JS files we create. ngx is a global object that is provided by NGINX for working with NJS. Read more on this and other variables and functions.

Creating API Keys

Well, demo cannot be the only person to use this endpoint. We can create a way for new apis to be created.

const store = ngx.shared.apikeys;

function createKey(r){
        const name = r.args.name //get name from args
        const key = Math.random().toString(36).substring(2, 10);
        store.set(key, JSON.stringify({
                credit: 15,
                name: name
        }))
        const data = {key: key, data: JSON.parse(store.get(key))}
        r.return(201, JSON.stringify(data))
}

.....

export default {doAuth , createKey}

Enter fullscreen mode Exit fullscreen mode

We then add a new location endpoint:

....
server {
        listen 8080;
        server_name _;

        location = /newkey {
                js_content auth.createKey;
        }
        ....
}
Enter fullscreen mode Exit fullscreen mode

When we call this endpoint, we create a new key that will be used for authentication:

$ curl  "localhost:8080/newkey?name=larry"
{"key":"205cde2b","data":{"credit":15,"name":"larry"}}

$ curl -H 'x-api-key: 205cde2b' localhost:8080/jph/posts/1 #using the key
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Enter fullscreen mode Exit fullscreen mode

Modifying Response Headers with NJS”

We would like to add an id to the headers. Maybe for auditing or something else. For this, the 'js_headers_filter' will be of help. With this directive we can add, delete or change headers.

Let's add some code to the location /jph/

.....

    location /jph/ {
            auth_request /auth; #uses location /auth to authenticate request
            js_header_filter auth.addReqId; //adds uuid to the header
            resolver 1.1.1.1; #using Cloudflare DNS resolver
            proxy_pass https://jsonplaceholder.typicode.com/;
            proxy_ssl_server_name on;
    }

.....
Enter fullscreen mode Exit fullscreen mode
.........

function randomUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
                const r = Math.random() * 16 | 0;
                const v = c === 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
        });
}
function addReqId(r){
        r.headersOut['x-request-uuid'] = randomUUID();
}

export default {doAuth , createKey, addReqId}

Enter fullscreen mode Exit fullscreen mode

When we request we get a new header added to the response:

$ curl localhost:8080/jph/posts/1 -I
HTTP/1.1 401 Unauthorized
Server: nginx/1.28.0
Date: Fri, 31 Oct 2025 17:43:03 GMT
Content-Type: text/html
Content-Length: 179
Connection: keep-alive
x-request-uuid: c4ee86fb-8799-46fc-b4f6-fd54bb5fb538
Enter fullscreen mode Exit fullscreen mode

Final

These are our final files:

# /etc/nginx/nginx.conf
load_module modules/ngx_http_js_module.so;

events {}

http {
        error_log /var/log/nginx/error.log debug;

        js_path /etc/nginx/njs/; #sets the path
        js_import hello from hello.js; #imports file as hello
        js_import auth from auth.js;
        js_shared_dict_zone zone=apikeys:1M;

        server {
        listen 8080;
        server_name _;

        location = /newkey {
                js_content auth.createKey;
        }

        location / {
            js_content hello.sayHello;
        }

        location /jph/ {
                auth_request /auth; #uses location /auth to authenticate request
                js_header_filter auth.addReqId;
                resolver 1.1.1.1; #using Cloudflare DNS resolver
                proxy_pass https://jsonplaceholder.typicode.com/;
                proxy_ssl_server_name on; #Includes the Server Name Indication (SNI)
        }

        location = /auth {
                internal;
                js_content auth.doAuth;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
//loading the shared object. ngx is a global nginx provided variable
const store = ngx.shared.apikeys;

//create demo user
if (!store.get("demo")) {
  store.set(
    "demo",
    JSON.stringify({
      credit: 15,
      name: "demo",
    })
  );
}

function createKey(r) {
  const name = r.args.name; //get name from args
  const key = Math.random().toString(36).substring(2, 10);
  store.set(
    key,
    JSON.stringify({
      credit: 15,
      name: name,
    })
  );
  const data = { key: key, data: JSON.parse(store.get(key)) };
  r.return(201, JSON.stringify(data));
}

function doAuth(r) {
  const key = r.headersIn["x-api-key"]; //get the key from the request header
  if (!key) {
    r.return(401, "missing key");
    return;
  }
  const user = store.get(key);
  if (!user) {
    r.return(401, "key not found");
    return;
  }
  r.return(200);
}
function randomUUID() {
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0;
    const v = c === "x" ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}
function addReqId(r) {
  r.headersOut["x-request-uuid"] = randomUUID();
}

export default { doAuth, createKey, addReqId };
Enter fullscreen mode Exit fullscreen mode

Conclusion

This is obviously a simple way to use NJS. It is packed with many features that will enable you to extend your NGINX server as you wish. It is worth noting that although it gives you the NPM ecosystem, you may need to transpile the JS code so that it runs without issues.

References

Official NGINX Documentation


Tutorials

Top comments (0)