Skip to content

Using JavaScript (njs) in NGINX for Custom Middleware

Goal: Learn how to use the official NGINX JavaScript module (njs) to build custom middleware — for logic like authentication, geofencing, or header inspection — without needing Node.js, Lua, or rebuilding NGINX.


Overview

Starting with NGINX 1.19+, you can extend NGINX behavior using njs, a lightweight JavaScript engine embedded directly into NGINX.
It allows you to write custom logic that runs inside the web server process — similar to middleware in Express.js — without proxies or microservices.

You can use it for:

  • Dynamic access control (auth, API keys, JWT)
  • Geofencing or IP-based blocking
  • Intelligent request routing
  • Custom logging or header rewriting
  • Edge-layer logic before proxying to backend servers

1. Prerequisites

You need:

  • NGINX ≥ 1.19 (dynamic modules supported)
  • Root or sudo access

Check version and modules:

bash
nginx -V

If you see these files on your system:

/usr/lib/nginx/modules/ngx_http_js_module.so
/usr/lib/nginx/modules/ngx_stream_js_module.so

then njs is already available.


2. Enable the JavaScript Module

Edit /etc/nginx/nginx.conf and load both modules at the top:

nginx
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
load_module /usr/lib/nginx/modules/ngx_stream_js_module.so;

Test and reload:

bash
nginx -t && systemctl reload nginx

3. Create a JavaScript Middleware Directory

Create a dedicated folder:

bash
mkdir -p /etc/nginx/njs

You can place all your middleware logic here:

/etc/nginx/njs/
├── auth.js
├── geofence.js
└── logger.js

4. Write Your First Middleware (Example: Geofence)

Create /etc/nginx/njs/geofence.js:

js
export default {
  check: function (r) {
    const country = (r.headersIn['CF-IPCountry'] || 'UNKNOWN').trim();
    const lat = parseFloat(r.headersIn['CF-IPLatitude'] || 0);
    const lon = parseFloat(r.headersIn['CF-IPLongitude'] || 0);

    r.log(`\ud83c\udf0d GeoCheck: country=${country}, lat=${lat}, lon=${lon}`);

    // Simple example bounds (Los Baños area)
    const bounds = { minLat: 14.162, maxLat: 14.179, minLon: 121.214, maxLon: 121.238 };

    if (country !== 'PH') {
      r.return(403, `Access denied. Country ${country} not allowed.\n`);
      return;
    }

    if (
      lat && lon &&
      (lat < bounds.minLat || lat > bounds.maxLat ||
       lon < bounds.minLon || lon > bounds.maxLon)
    ) {
      r.return(403, 'Access denied. Outside barangay boundary.\n');
      return;
    }

    r.return(200, '\u2705 Access granted. Inside barangay zone!\n');
  }
};

This script:

  • Reads Cloudflare geolocation headers
  • Logs incoming requests
  • Denies access if the request comes from outside the Philippines or a defined latitude/longitude range

5. Import and Run It in NGINX

Create /etc/nginx/sites-enabled/njs-test.conf:

nginx
js_import geo from /etc/nginx/njs/geofence.js;

server {
    listen 9020;
    server_name _;

    location / {
        # Run JS as content handler
        js_content geo.check;
    }
}

Reload:

bash
nginx -t && systemctl restart nginx

6. Test the Middleware

bash
curl -H "CF-IPCountry: PH" \
     -H "CF-IPLatitude: 14.170" \
     -H "CF-IPLongitude: 121.225" \
     http://localhost:9020/

Output:

Access granted. Inside barangay zone!

Test outside the area:

bash
curl -H "CF-IPCountry: PH" -H "CF-IPLatitude: 14.190" -H "CF-IPLongitude: 121.250" http://localhost:9020/

Output:

Access denied. Outside barangay boundary.

7. Passing Variables to JavaScript Middleware

You can make your middleware flexible by passing variables from NGINX to JavaScript using query parameters, headers, or NGINX variables.

a) Via Query Parameters

bash
curl "http://localhost:9020/?zone=barangay1"

In JS:

js
const zone = r.args.zone || 'unknown';
r.return(200, `Requested zone: ${zone}\n`);

b) Via NGINX Variables

nginx
location /barangay1 {
    set $zone_name "Barangay 1";
    js_content geo.check;
}

In JS:

js
const zone = r.variables.zone_name;
r.return(200, `Serving zone: ${zone}\n`);

c) Via Request Headers

bash
curl -H "X-Zone: Barangay 2" http://localhost:9020/

In JS:

js
const zone = r.headersIn['X-Zone'] || 'unknown';

8. Dynamic Parameter-Based Routing

You can even extract variables directly from the URI:

nginx
location ~ ^/geo/(.*) {
    set $zone $1;
    js_content geo.dynamic;
}

In JS:

js
export default {
  dynamic: function (r) {
    const zone = r.variables.zone;
    r.return(200, `Dynamic zone route: ${zone}\n`);
  }
};

Example:

bash
curl http://localhost:9020/geo/barangay5

Output:

Dynamic zone route: barangay5

9. Debugging and Logging

Check runtime logs:

bash
tail -f /var/log/nginx/error.log

Inside your JS, you can log:

js
r.log(`Geo middleware triggered for ${r.remoteAddress}`);

10. Summary

StepAction
1Enable ngx_http_js_module
2Create /etc/nginx/njs/ for scripts
3Write JS middleware with exported functions
4Import via js_import
5Run using js_content
6Pass data via headers, variables, or query params
7Log and debug in /var/log/nginx/error.log

Example File Layout

/etc/nginx/
├── nginx.conf
├── njs/
│   ├── auth.js
│   ├── geofence.js
│   ├── logger.js
│   └── main.js
└── sites-enabled/
    └── njs-test.conf

Next Steps

  • Build middleware for JWT or API key authentication
  • Implement geo-blocking with a database or GeoJSON polygon
  • Create a reusable JS router to load barangay boundary data dynamically
  • Log access results to external services or files

Author: Marcuwynu DevOps Team
Version: 1.1
Last Updated: October 2025